diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
index ddb54e1ee..13e3ada28 100644
--- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
+++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs
@@ -18,7 +18,7 @@ namespace Artemis.Core
/// Gets the description attribute applied to this property
///
PropertyDescriptionAttribute PropertyDescription { get; }
-
+
///
/// Gets the profile element (such as layer or folder) this property is applied to
///
@@ -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
///
bool IsLoadedFromStorage { get; }
-
+
///
/// Initializes the layer property
///
@@ -102,6 +102,20 @@ namespace Artemis.Core
///
void UpdateDataBinding();
+ ///
+ /// Removes a keyframe from the layer property without knowing it's type.
+ /// Prefer .
+ ///
+ ///
+ void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
+
+ ///
+ /// Adds a keyframe to the layer property without knowing it's type.
+ /// Prefer .
+ ///
+ ///
+ void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
+
///
/// Occurs when the layer property is disposed
///
diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
index 981805482..1abd9ecba 100644
--- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
+++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs
@@ -96,6 +96,24 @@ namespace Artemis.Core
OnUpdated();
}
+ ///
+ public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe)
+ {
+ if (keyframe is not LayerPropertyKeyframe typedKeyframe)
+ throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}.");
+
+ RemoveKeyframe(typedKeyframe);
+ }
+
+ ///
+ public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe)
+ {
+ if (keyframe is not LayerPropertyKeyframe typedKeyframe)
+ throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}.");
+
+ AddKeyframe(typedKeyframe);
+ }
+
///
public void Dispose()
{
@@ -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.
///
- public void SetCurrentValue(T value, TimeSpan? time)
+ /// The new keyframe if one was created.
+ public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
+ LayerPropertyKeyframe? newKeyframe = null;
if (time == null || !KeyframesEnabled || !KeyframesSupported)
- {
BaseValue = value;
- }
else
{
// If on a keyframe, update the keyframe
LayerPropertyKeyframe? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
// Create a new keyframe if none found
if (currentKeyframe == null)
- AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this));
+ {
+ newKeyframe = new LayerPropertyKeyframe(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;
}
///
@@ -247,6 +269,7 @@ namespace Artemis.Core
{
if (_keyframesEnabled == value) return;
_keyframesEnabled = value;
+ ReapplyUpdate();
OnKeyframesToggled();
OnPropertyChanged(nameof(KeyframesEnabled));
}
diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs
index 86c9023d7..ef4bf2740 100644
--- a/src/Artemis.Core/Models/Profile/Timeline.cs
+++ b/src/Artemis.Core/Models/Profile/Timeline.cs
@@ -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;
+
+///
+/// Represents a timeline used by profile elements
+///
+public class Timeline : CorePropertyChanged, IStorageModel
{
+ private const int MaxExtraTimelines = 15;
+ private readonly object _lock = new();
+
///
- /// Represents a timeline used by profile elements
+ /// Creates a new instance of the class
///
- public class Timeline : CorePropertyChanged, IStorageModel
+ public Timeline()
{
- private const int MaxExtraTimelines = 15;
- private readonly object _lock = new();
+ Entity = new TimelineEntity();
+ MainSegmentLength = TimeSpan.FromSeconds(5);
- ///
- /// Creates a new instance of the class
- ///
- public Timeline()
+ _extraTimelines = new List();
+ ExtraTimelines = new ReadOnlyCollection(_extraTimelines);
+
+ Save();
+ }
+
+ internal Timeline(TimelineEntity entity)
+ {
+ Entity = entity;
+ _extraTimelines = new List();
+ ExtraTimelines = new ReadOnlyCollection(_extraTimelines);
+
+ Load();
+ }
+
+ private Timeline(Timeline parent)
+ {
+ Entity = new TimelineEntity();
+ Parent = parent;
+ StartSegmentLength = Parent.StartSegmentLength;
+ MainSegmentLength = Parent.MainSegmentLength;
+ EndSegmentLength = Parent.EndSegmentLength;
+
+ _extraTimelines = new List();
+ ExtraTimelines = new ReadOnlyCollection(_extraTimelines);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"Progress: {Position}/{Length} - delta: {Delta}";
+ }
+
+ #region Extra timelines
+
+ ///
+ /// Adds an extra timeline to this timeline
+ ///
+ public void AddExtraTimeline()
+ {
+ _extraTimelines.Add(new Timeline(this));
+ if (_extraTimelines.Count > MaxExtraTimelines)
+ _extraTimelines.RemoveAt(0);
+ }
+
+ ///
+ /// Removes all extra timelines from this timeline
+ ///
+ public void ClearExtraTimelines()
+ {
+ _extraTimelines.Clear();
+ }
+
+ #endregion
+
+ #region Properties
+
+ private TimeSpan _position;
+ private TimeSpan _lastDelta;
+ private TimelinePlayMode _playMode;
+ private TimelineStopMode _stopMode;
+ private readonly List _extraTimelines;
+ private TimeSpan _startSegmentLength;
+ private TimeSpan _mainSegmentLength;
+ private TimeSpan _endSegmentLength;
+
+ ///
+ /// Gets the parent this timeline is an extra timeline of
+ ///
+ public Timeline? Parent { get; }
+
+ ///
+ /// Gets the current position of the timeline
+ ///
+ public TimeSpan Position
+ {
+ get => _position;
+ private set => SetAndNotify(ref _position, value);
+ }
+
+ ///
+ /// Gets the cumulative delta of all calls to that took place after the last call to
+ ///
+ ///
+ /// Note: If this is an extra timeline is always equal to
+ ///
+ ///
+ public TimeSpan Delta
+ {
+ get => Parent == null ? _lastDelta : DeltaToParent;
+ private set => SetAndNotify(ref _lastDelta, value);
+ }
+
+ ///
+ /// Gets the delta to this timeline's
+ ///
+ public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
+
+ ///
+ /// Gets or sets the mode in which the render element starts its timeline when display conditions are met
+ ///
+ public TimelinePlayMode PlayMode
+ {
+ get => _playMode;
+ set => SetAndNotify(ref _playMode, value);
+ }
+
+ ///
+ /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
+ ///
+ public TimelineStopMode StopMode
+ {
+ get => _stopMode;
+ set => SetAndNotify(ref _stopMode, value);
+ }
+
+ ///
+ /// Gets a list of extra copies of the timeline applied to this timeline
+ ///
+ public ReadOnlyCollection ExtraTimelines { get; }
+
+ ///
+ /// Gets a boolean indicating whether the timeline has finished its run
+ ///
+ public bool IsFinished => Position > Length && !ExtraTimelines.Any();
+
+ ///
+ /// Gets a boolean indicating whether the timeline progress has been overridden
+ ///
+ public bool IsOverridden { get; private set; }
+
+ #region Segments
+
+ ///
+ /// Gets the total length of this timeline
+ ///
+ public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
+
+ ///
+ /// Gets or sets the length of the start segment
+ ///
+ 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();
- ExtraTimelines = new ReadOnlyCollection(_extraTimelines);
+ ///
+ /// Gets or sets the length of the main segment
+ ///
+ public TimeSpan MainSegmentLength
+ {
+ get => _mainSegmentLength;
+ set
+ {
+ if (SetAndNotify(ref _mainSegmentLength, value))
+ NotifySegmentShiftAt(TimelineSegment.Main, false);
+ }
+ }
- Save();
+ ///
+ /// Gets or sets the length of the end segment
+ ///
+ public TimeSpan EndSegmentLength
+ {
+ get => _endSegmentLength;
+ set
+ {
+ if (SetAndNotify(ref _endSegmentLength, value))
+ NotifySegmentShiftAt(TimelineSegment.End, false);
+ }
+ }
+
+ ///
+ /// Gets or sets the start position of the main segment
+ ///
+ public TimeSpan MainSegmentStartPosition
+ {
+ get => StartSegmentEndPosition;
+ set
+ {
+ StartSegmentEndPosition = value;
+ NotifySegmentShiftAt(TimelineSegment.Main, true);
+ }
+ }
+
+ ///
+ /// Gets or sets the end position of the end segment
+ ///
+ public TimeSpan EndSegmentStartPosition
+ {
+ get => MainSegmentEndPosition;
+ set
+ {
+ MainSegmentEndPosition = value;
+ NotifySegmentShiftAt(TimelineSegment.End, true);
+ }
+ }
+
+ ///
+ /// Gets or sets the end position of the start segment
+ ///
+ public TimeSpan StartSegmentEndPosition
+ {
+ get => StartSegmentLength;
+ set
+ {
+ StartSegmentLength = value;
+ NotifySegmentShiftAt(TimelineSegment.Start, false);
+ }
+ }
+
+ ///
+ /// Gets or sets the end position of the main segment
+ ///
+ public TimeSpan MainSegmentEndPosition
+ {
+ get => StartSegmentEndPosition + MainSegmentLength;
+ set
+ {
+ MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
+ NotifySegmentShiftAt(TimelineSegment.Main, false);
+ }
+ }
+
+ ///
+ /// Gets or sets the end position of the end segment
+ ///
+ 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; }
+
+ ///
+ /// Notifies the right segments in a way that I don't have to think about it
+ ///
+ /// The segment that was updated
+ /// Whether the start point of the was updated
+ 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();
- ExtraTimelines = new ReadOnlyCollection(_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();
- ExtraTimelines = new ReadOnlyCollection(_extraTimelines);
+ OnPropertyChanged(nameof(Length));
+ OnTimelineChanged();
+ }
+
+ ///
+ /// Occurs when changes have been made to any of the segments of the timeline.
+ ///
+ public event EventHandler? TimelineChanged;
+
+ private void OnTimelineChanged()
+ {
+ TimelineChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Updating
+
+ private TimeSpan _lastOverridePosition;
+
+ ///
+ /// Updates the timeline, applying the provided to the
+ ///
+ /// The amount of time to apply to the position
+ /// Whether to stick to the main segment, wrapping around if needed
+ 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);
}
+ }
- ///
- public override string ToString()
+ ///
+ /// Moves the position of the timeline backwards to the very start of the timeline
+ ///
+ 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
-
- ///
- /// Adds an extra timeline to this timeline
- ///
- public void AddExtraTimeline()
+ ///
+ /// Moves the position of the timeline forwards to the beginning of the end segment
+ ///
+ 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;
}
+ }
- ///
- /// Removes all extra timelines from this timeline
- ///
- public void ClearExtraTimelines()
+ ///
+ /// Moves the position of the timeline forwards to the very end of the timeline
+ ///
+ public void JumpToEnd()
+ {
+ lock (_lock)
{
+ if (Position >= EndSegmentEndPosition)
+ return;
+
+ Delta = EndSegmentEndPosition - Position;
+ Position = EndSegmentEndPosition;
+ }
+ }
+
+ ///
+ /// Overrides the to the specified time and clears any extra time lines
+ ///
+ /// The position to set the timeline to
+ /// Whether to stick to the main segment, wrapping around if needed
+ 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 _extraTimelines;
- private TimeSpan _startSegmentLength;
- private TimeSpan _mainSegmentLength;
- private TimeSpan _endSegmentLength;
-
- ///
- /// Gets the parent this timeline is an extra timeline of
- ///
- public Timeline? Parent { get; }
-
- ///
- /// Gets the current position of the timeline
- ///
- public TimeSpan Position
- {
- get => _position;
- private set => SetAndNotify(ref _position, value);
- }
-
- ///
- /// Gets the cumulative delta of all calls to that took place after the last call to
- ///
- ///
- /// Note: If this is an extra timeline is always equal to
- ///
- ///
- public TimeSpan Delta
- {
- get => Parent == null ? _lastDelta : DeltaToParent;
- private set => SetAndNotify(ref _lastDelta, value);
- }
-
- ///
- /// Gets the delta to this timeline's
- ///
- public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
-
- ///
- /// Gets or sets the mode in which the render element starts its timeline when display conditions are met
- ///
- public TimelinePlayMode PlayMode
- {
- get => _playMode;
- set => SetAndNotify(ref _playMode, value);
- }
-
- ///
- /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
- ///
- public TimelineStopMode StopMode
- {
- get => _stopMode;
- set => SetAndNotify(ref _stopMode, value);
- }
-
- ///
- /// Gets a list of extra copies of the timeline applied to this timeline
- ///
- public ReadOnlyCollection ExtraTimelines { get; }
-
- ///
- /// Gets a boolean indicating whether the timeline has finished its run
- ///
- public bool IsFinished => Position > Length && !ExtraTimelines.Any();
-
- ///
- /// Gets a boolean indicating whether the timeline progress has been overridden
- ///
- public bool IsOverridden { get; private set; }
-
- #region Segments
-
- ///
- /// Gets the total length of this timeline
- ///
- public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
-
- ///
- /// Gets or sets the length of the start segment
- ///
- public TimeSpan StartSegmentLength
- {
- get => _startSegmentLength;
- set
- {
- if (SetAndNotify(ref _startSegmentLength, value))
- NotifySegmentShiftAt(TimelineSegment.Start, false);
- }
- }
-
- ///
- /// Gets or sets the length of the main segment
- ///
- public TimeSpan MainSegmentLength
- {
- get => _mainSegmentLength;
- set
- {
- if (SetAndNotify(ref _mainSegmentLength, value))
- NotifySegmentShiftAt(TimelineSegment.Main, false);
- }
- }
-
- ///
- /// Gets or sets the length of the end segment
- ///
- public TimeSpan EndSegmentLength
- {
- get => _endSegmentLength;
- set
- {
- if (SetAndNotify(ref _endSegmentLength, value))
- NotifySegmentShiftAt(TimelineSegment.End, false);
- }
- }
-
- ///
- /// Gets or sets the start position of the main segment
- ///
- public TimeSpan MainSegmentStartPosition
- {
- get => StartSegmentEndPosition;
- set
- {
- StartSegmentEndPosition = value;
- NotifySegmentShiftAt(TimelineSegment.Main, true);
- }
- }
-
- ///
- /// Gets or sets the end position of the end segment
- ///
- public TimeSpan EndSegmentStartPosition
- {
- get => MainSegmentEndPosition;
- set
- {
- MainSegmentEndPosition = value;
- NotifySegmentShiftAt(TimelineSegment.End, true);
- }
- }
-
- ///
- /// Gets or sets the end position of the start segment
- ///
- public TimeSpan StartSegmentEndPosition
- {
- get => StartSegmentLength;
- set
- {
- StartSegmentLength = value;
- NotifySegmentShiftAt(TimelineSegment.Start, false);
- }
- }
-
- ///
- /// Gets or sets the end position of the main segment
- ///
- public TimeSpan MainSegmentEndPosition
- {
- get => StartSegmentEndPosition + MainSegmentLength;
- set
- {
- MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
- NotifySegmentShiftAt(TimelineSegment.Main, false);
- }
- }
-
- ///
- /// Gets or sets the end position of the end segment
- ///
- 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; }
-
- ///
- /// Notifies the right segments in a way that I don't have to think about it
- ///
- /// The segment that was updated
- /// Whether the start point of the was updated
- 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;
-
- ///
- /// Updates the timeline, applying the provided to the
- ///
- /// The amount of time to apply to the position
- /// Whether to stick to the main segment, wrapping around if needed
- 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);
- }
- }
-
- ///
- /// Moves the position of the timeline backwards to the very start of the timeline
- ///
- public void JumpToStart()
- {
- lock (_lock)
- {
- if (Position == TimeSpan.Zero)
- return;
-
- Delta = TimeSpan.Zero - Position;
- Position = TimeSpan.Zero;
- }
- }
-
- ///
- /// Moves the position of the timeline forwards to the beginning of the end segment
- ///
- public void JumpToEndSegment()
- {
- lock (_lock)
- {
- if (Position >= EndSegmentStartPosition)
- return;
-
- Delta = EndSegmentStartPosition - Position;
- Position = EndSegmentStartPosition;
- }
- }
-
- ///
- /// Moves the position of the timeline forwards to the very end of the timeline
- ///
- public void JumpToEnd()
- {
- lock (_lock)
- {
- if (Position >= EndSegmentEndPosition)
- return;
-
- Delta = EndSegmentEndPosition - Position;
- Position = EndSegmentEndPosition;
- }
- }
-
- ///
- /// Overrides the to the specified time and clears any extra time lines
- ///
- /// The position to set the timeline to
- /// Whether to stick to the main segment, wrapping around if needed
- 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();
- }
- }
-
- ///
- /// Sets the to
- ///
- public void ClearDelta()
- {
- lock (_lock)
- {
- Delta = TimeSpan.Zero;
- }
- }
-
- #endregion
-
- #region Storage
-
- ///
- public void Load()
- {
- StartSegmentLength = Entity.StartSegmentLength;
- MainSegmentLength = Entity.MainSegmentLength;
- EndSegmentLength = Entity.EndSegmentLength;
- PlayMode = (TimelinePlayMode) Entity.PlayMode;
- StopMode = (TimelineStopMode) Entity.StopMode;
-
- JumpToEnd();
- }
-
- ///
- 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
}
///
- /// Represents a mode for render elements to start their timeline when display conditions are met
+ /// Sets the to
///
- public enum TimelinePlayMode
+ public void ClearDelta()
{
- ///
- /// Continue repeating the main segment of the timeline while the condition is met
- ///
- Repeat,
-
- ///
- /// Only play the timeline once when the condition is met
- ///
- Once
+ lock (_lock)
+ {
+ Delta = TimeSpan.Zero;
+ }
}
+ #endregion
+
+ #region Storage
+
+ ///
+ public void Load()
+ {
+ StartSegmentLength = Entity.StartSegmentLength;
+ MainSegmentLength = Entity.MainSegmentLength;
+ EndSegmentLength = Entity.EndSegmentLength;
+ PlayMode = (TimelinePlayMode) Entity.PlayMode;
+ StopMode = (TimelineStopMode) Entity.StopMode;
+
+ JumpToEnd();
+ }
+
+ ///
+ 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
+}
+
+///
+/// Represents a mode for render elements to start their timeline when display conditions are met
+///
+public enum TimelinePlayMode
+{
+ ///
+ /// Continue repeating the main segment of the timeline while the condition is met
+ ///
+ Repeat,
+
///
- /// 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
///
- public enum TimelineStopMode
- {
- ///
- /// When conditions are no longer met, finish the the current run of the main timeline
- ///
- Finish,
+ Once
+}
- ///
- /// When conditions are no longer met, skip to the end segment of the timeline
- ///
- SkipToEnd
- }
+///
+/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
+///
+public enum TimelineStopMode
+{
+ ///
+ /// When conditions are no longer met, finish the the current run of the main timeline
+ ///
+ Finish,
///
- /// 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
///
- public enum TimeLineEventOverlapMode
- {
- ///
- /// Stop the current run and restart the timeline
- ///
- Restart,
+ SkipToEnd
+}
- ///
- /// Ignore subsequent event fires until the timeline finishes
- ///
- Ignore,
+///
+/// Represents a mode for render elements to start their timeline when display conditions events are fired
+///
+public enum TimeLineEventOverlapMode
+{
+ ///
+ /// Stop the current run and restart the timeline
+ ///
+ Restart,
- ///
- /// Play another copy of the timeline on top of the current run
- ///
- Copy,
+ ///
+ /// Ignore subsequent event fires until the timeline finishes
+ ///
+ Ignore,
- ///
- /// Repeat the timeline until the event fires again
- ///
- Toggle
- }
+ ///
+ /// Play another copy of the timeline on top of the current run
+ ///
+ Copy,
+
+ ///
+ /// Repeat the timeline until the event fires again
+ ///
+ Toggle
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
index 16616b63e..47aee7b1b 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
@@ -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)
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs
index 4f6b552bb..eacb82fce 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs
@@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
///
/// Represents a profile editor command that can be used to delete a keyframe.
///
-public class DeleteKeyframe : IProfileEditorCommand
+public class DeleteKeyframe : IProfileEditorCommand
{
- private readonly LayerPropertyKeyframe _keyframe;
+ private readonly ILayerPropertyKeyframe _keyframe;
///
/// Creates a new instance of the class.
///
- public DeleteKeyframe(LayerPropertyKeyframe keyframe)
+ public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
{
_keyframe = keyframe;
}
@@ -25,13 +25,13 @@ public class DeleteKeyframe : IProfileEditorCommand
///
public void Execute()
{
- _keyframe.LayerProperty.RemoveKeyframe(_keyframe);
+ _keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
}
///
public void Undo()
{
- _keyframe.LayerProperty.AddKeyframe(_keyframe);
+ _keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
}
#endregion
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResizeTimelineSegment.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResizeTimelineSegment.cs
new file mode 100644
index 000000000..77f9bc2b1
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResizeTimelineSegment.cs
@@ -0,0 +1,108 @@
+using System;
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
+
+///
+/// Represents a profile editor command that can be used to change the length of a timeline segment.
+///
+public class ResizeTimelineSegment : IProfileEditorCommand
+{
+ private readonly TimeSpan _length;
+ private readonly TimeSpan _originalLength;
+ private readonly RenderProfileElement _profileElement;
+ private readonly SegmentType _segmentType;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The type of segment to resize.
+ /// The render profile element whose segment to resize.
+ /// The new length of the segment
+ 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
+ };
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The type of segment to resize.
+ /// The render profile element whose segment to resize.
+ /// The new length of the segment
+ /// The original length of the segment
+ public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length, TimeSpan originalLength)
+ {
+ _segmentType = segmentType;
+ _profileElement = profileElement;
+ _length = length;
+ _originalLength = originalLength;
+ }
+
+ ///
+ public string DisplayName => $"Resize {_segmentType.ToString().ToLower()} segment";
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Represents a type of segment on a timeline.
+ ///
+ public enum SegmentType
+ {
+ ///
+ /// The start segment.
+ ///
+ Start,
+
+ ///
+ /// The main segment.
+ ///
+ Main,
+
+ ///
+ /// The end segment.
+ ///
+ End
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs
index 9eaf43424..d4dc4a800 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs
@@ -12,6 +12,7 @@ public class UpdateLayerProperty : IProfileEditorCommand
private readonly T _newValue;
private readonly T _originalValue;
private readonly TimeSpan? _time;
+ private LayerPropertyKeyframe? _newKeyframe;
///
/// Creates a new instance of the class.
@@ -43,13 +44,16 @@ public class UpdateLayerProperty : IProfileEditorCommand
///
public void Execute()
{
- _layerProperty.SetCurrentValue(_newValue, _time);
+ _newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
}
///
public void Undo()
{
- _layerProperty.SetCurrentValue(_originalValue, _time);
+ if (_newKeyframe != null)
+ _layerProperty.RemoveKeyframe(_newKeyframe);
+ else
+ _layerProperty.SetCurrentValue(_originalValue, _time);
}
#endregion
diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings
index f3b6b6826..9b01cf388 100644
--- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings
+++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings
@@ -4,6 +4,7 @@
True
True
True
+ False
True
True
True
diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
index 9666d77c9..58e336a92 100644
--- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
+++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
@@ -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 propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
-
- // TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection profileElementPropertyGroups);
- // EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
- // TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection profileElementPropertyGroups);
- // TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection profileElementPropertyGroups);
}
public interface IPropertyVmFactory
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml
index 20d51558a..4fe95180c 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml
@@ -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">
+
+
+
@@ -31,76 +34,103 @@
Background="Transparent"
Margin="0 0 -5 0" />
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs
index 71e47c7a0..eb453dc1f 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs
@@ -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;
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelinePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelinePropertyViewModel.cs
index 791587907..4770f4cf9 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelinePropertyViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelinePropertyViewModel.cs
@@ -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;
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/ITimelineKeyframeViewModel.cs
similarity index 83%
rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs
rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/ITimelineKeyframeViewModel.cs
index c04b6857f..91b4af8b5 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/ITimelineKeyframeViewModel.cs
@@ -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
{
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml
similarity index 95%
rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml
rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml
index a5672ee0e..bcc8e697e 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml
@@ -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">
{
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
similarity index 96%
rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeViewModel.cs
rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
index 533259b5f..be3aa871d 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
@@ -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 : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
@@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK
public void Delete()
{
- _profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
+ _profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
}
#endregion
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml
new file mode 100644
index 000000000..bb15969fa
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ End
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml.cs
new file mode 100644
index 000000000..1bf4c584e
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml.cs
@@ -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
+ {
+ private readonly Rectangle _keyframeDragAnchor;
+ private double _dragOffset;
+
+ public EndSegmentView()
+ {
+ InitializeComponent();
+ _keyframeDragAnchor = this.Get("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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs
new file mode 100644
index 000000000..2aa379786
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs
@@ -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? _start;
+ private ObservableAsPropertyHelper? _end;
+ private ObservableAsPropertyHelper? _endTimestamp;
+ private readonly ObservableAsPropertyHelper _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())
+ .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())
+ .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())
+ .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;
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml
new file mode 100644
index 000000000..6efe166ab
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Main
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml.cs
new file mode 100644
index 000000000..b84bfe04f
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml.cs
@@ -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
+ {
+ private readonly Rectangle _keyframeDragAnchor;
+ private double _dragOffset;
+
+ public MainSegmentView()
+ {
+ InitializeComponent();
+ _keyframeDragAnchor = this.Get("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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs
new file mode 100644
index 000000000..bad569d7b
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs
@@ -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? _start;
+ private ObservableAsPropertyHelper? _end;
+ private ObservableAsPropertyHelper? _endTimestamp;
+ private readonly ObservableAsPropertyHelper _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())
+ .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())
+ .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())
+ .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;
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml
new file mode 100644
index 000000000..6502c23be
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml
new file mode 100644
index 000000000..aeaa95f4e
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+ Start
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs
new file mode 100644
index 000000000..234acb2de
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs
@@ -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
+ {
+ private readonly Rectangle _keyframeDragAnchor;
+ private double _dragOffset;
+
+ public StartSegmentView()
+ {
+ InitializeComponent();
+ _keyframeDragAnchor = this.Get("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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs
new file mode 100644
index 000000000..926466e43
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs
@@ -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? _end;
+ private ObservableAsPropertyHelper? _endTimestamp;
+ private readonly ObservableAsPropertyHelper _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())
+ .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())
+ .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;
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs
new file mode 100644
index 000000000..e60369967
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs
@@ -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 _originalKeyframePositions = new();
+
+ private ObservableAsPropertyHelper? _showAddStart;
+ private ObservableAsPropertyHelper? _showAddMain;
+ private ObservableAsPropertyHelper? _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())
+ .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())
+ .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())
+ .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 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 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 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();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml
index 6a69dbb07..ff04b5406 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml
@@ -12,8 +12,8 @@
-
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs
index d908f3e02..354b7c30b 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs
@@ -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 : ActivatableViewModelBase, ITimelineP
this.WhenActivated(d =>
{
+ Observable.FromEventPattern(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
+ .Subscribe(_ => UpdateKeyframes())
+ .DisposeWith(d);
Observable.FromEventPattern(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml
index 44674b0c9..0dbaf8c3b 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml
@@ -10,8 +10,11 @@
28
29
-
-
+
+
+
+
+
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs
index c311cf428..c23f6e109 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs
@@ -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;
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs
index 4452f5906..ccd0aa2b7 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs
@@ -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? _caretPosition;
private ObservableAsPropertyHelper? _pixelsPerSecond;
private List? _moveKeyframes;
+ private ObservableAsPropertyHelper _minWidth;
- public TimelineViewModel(ObservableCollection propertyGroupViewModels, IProfileEditorService profileEditorService)
+ public TimelineViewModel(ObservableCollection 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())
+ .Switch()
+ .CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
+ .ToProperty(this, vm => vm.MinWidth)
+ .DisposeWith(d);
});
}
public ObservableCollection 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 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
{