1
0
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:
Robert 2022-01-25 00:23:26 +01:00
parent 913117ad0a
commit 642823add5
35 changed files with 1569 additions and 579 deletions

View File

@ -18,7 +18,7 @@ namespace Artemis.Core
/// Gets the description attribute applied to this property /// Gets the description attribute applied to this property
/// </summary> /// </summary>
PropertyDescriptionAttribute PropertyDescription { get; } PropertyDescriptionAttribute PropertyDescription { get; }
/// <summary> /// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to /// Gets the profile element (such as layer or folder) this property is applied to
/// </summary> /// </summary>
@ -68,7 +68,7 @@ namespace Artemis.Core
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary> /// </summary>
bool IsLoadedFromStorage { get; } bool IsLoadedFromStorage { get; }
/// <summary> /// <summary>
/// Initializes the layer property /// Initializes the layer property
/// <para> /// <para>
@ -102,6 +102,20 @@ namespace Artemis.Core
/// </summary> /// </summary>
void UpdateDataBinding(); void UpdateDataBinding();
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary> /// <summary>
/// Occurs when the layer property is disposed /// Occurs when the layer property is disposed
/// </summary> /// </summary>

View File

@ -96,6 +96,24 @@ namespace Artemis.Core
OnUpdated(); OnUpdated();
} }
/// <inheritdoc />
public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe)
{
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}.");
RemoveKeyframe(typedKeyframe);
}
/// <inheritdoc />
public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe)
{
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}.");
AddKeyframe(typedKeyframe);
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
@ -177,22 +195,25 @@ namespace Artemis.Core
/// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new
/// or existing keyframe. /// or existing keyframe.
/// </param> /// </param>
public void SetCurrentValue(T value, TimeSpan? time) /// <returns>The new keyframe if one was created.</returns>
public LayerPropertyKeyframe<T>? SetCurrentValue(T value, TimeSpan? time)
{ {
if (_disposed) if (_disposed)
throw new ObjectDisposedException("LayerProperty"); throw new ObjectDisposedException("LayerProperty");
LayerPropertyKeyframe<T>? newKeyframe = null;
if (time == null || !KeyframesEnabled || !KeyframesSupported) if (time == null || !KeyframesEnabled || !KeyframesSupported)
{
BaseValue = value; BaseValue = value;
}
else else
{ {
// If on a keyframe, update the keyframe // If on a keyframe, update the keyframe
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
// Create a new keyframe if none found // Create a new keyframe if none found
if (currentKeyframe == null) if (currentKeyframe == null)
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this)); {
newKeyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
AddKeyframe(newKeyframe);
}
else else
currentKeyframe.Value = value; currentKeyframe.Value = value;
} }
@ -200,6 +221,7 @@ namespace Artemis.Core
// Force an update so that the base value is applied to the current value and // Force an update so that the base value is applied to the current value and
// keyframes/data bindings are applied using the new base value // keyframes/data bindings are applied using the new base value
ReapplyUpdate(); ReapplyUpdate();
return newKeyframe;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -247,6 +269,7 @@ namespace Artemis.Core
{ {
if (_keyframesEnabled == value) return; if (_keyframesEnabled == value) return;
_keyframesEnabled = value; _keyframesEnabled = value;
ReapplyUpdate();
OnKeyframesToggled(); OnKeyframesToggled();
OnPropertyChanged(nameof(KeyframesEnabled)); OnPropertyChanged(nameof(KeyframesEnabled));
} }

View File

@ -2,517 +2,529 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
namespace Artemis.Core namespace Artemis.Core;
/// <summary>
/// Represents a timeline used by profile elements
/// </summary>
public class Timeline : CorePropertyChanged, IStorageModel
{ {
private const int MaxExtraTimelines = 15;
private readonly object _lock = new();
/// <summary> /// <summary>
/// Represents a timeline used by profile elements /// Creates a new instance of the <see cref="Timeline" /> class
/// </summary> /// </summary>
public class Timeline : CorePropertyChanged, IStorageModel public Timeline()
{ {
private const int MaxExtraTimelines = 15; Entity = new TimelineEntity();
private readonly object _lock = new(); MainSegmentLength = TimeSpan.FromSeconds(5);
/// <summary> _extraTimelines = new List<Timeline>();
/// Creates a new instance of the <see cref="Timeline" /> class ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
/// </summary>
public Timeline() Save();
}
internal Timeline(TimelineEntity entity)
{
Entity = entity;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
Load();
}
private Timeline(Timeline parent)
{
Entity = new TimelineEntity();
Parent = parent;
StartSegmentLength = Parent.StartSegmentLength;
MainSegmentLength = Parent.MainSegmentLength;
EndSegmentLength = Parent.EndSegmentLength;
_extraTimelines = new List<Timeline>();
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
}
/// <inheritdoc />
public override string ToString()
{
return $"Progress: {Position}/{Length} - delta: {Delta}";
}
#region Extra timelines
/// <summary>
/// Adds an extra timeline to this timeline
/// </summary>
public void AddExtraTimeline()
{
_extraTimelines.Add(new Timeline(this));
if (_extraTimelines.Count > MaxExtraTimelines)
_extraTimelines.RemoveAt(0);
}
/// <summary>
/// Removes all extra timelines from this timeline
/// </summary>
public void ClearExtraTimelines()
{
_extraTimelines.Clear();
}
#endregion
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary>
/// Gets the current position of the timeline
/// </summary>
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
/// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
/// <see cref="ClearDelta" />
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary>
public TimeSpan Delta
{
get => Parent == null ? _lastDelta : DeltaToParent;
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <summary>
/// Gets a list of extra copies of the timeline applied to this timeline
/// </summary>
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
/// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden
/// </summary>
public bool IsOverridden { get; private set; }
#region Segments
/// <summary>
/// Gets the total length of this timeline
/// </summary>
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{ {
Entity = new TimelineEntity(); if (SetAndNotify(ref _startSegmentLength, value))
MainSegmentLength = TimeSpan.FromSeconds(5); NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
_extraTimelines = new List<Timeline>(); /// <summary>
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines); /// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
Save(); /// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
/// <summary>
/// Gets or sets the start position of the main segment
/// </summary>
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
/// <summary>
/// Gets or sets the end position of the start segment
/// </summary>
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the end position of the main segment
/// </summary>
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
/// <summary>
/// Notifies the right segments in a way that I don't have to think about it
/// </summary>
/// <param name="segment">The segment that was updated</param>
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
OnPropertyChanged(nameof(EndSegmentStartPosition));
OnPropertyChanged(nameof(EndSegmentEndPosition));
} }
internal Timeline(TimelineEntity entity) if (segment <= TimelineSegment.Main)
{ {
Entity = entity; if (startUpdated || segment < TimelineSegment.Main)
_extraTimelines = new List<Timeline>(); OnPropertyChanged(nameof(MainSegmentStartPosition));
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines); OnPropertyChanged(nameof(MainSegmentEndPosition));
Load();
} }
private Timeline(Timeline parent) if (segment <= TimelineSegment.Start)
{ OnPropertyChanged(nameof(StartSegmentEndPosition));
Entity = new TimelineEntity();
Parent = parent;
StartSegmentLength = Parent.StartSegmentLength;
MainSegmentLength = Parent.MainSegmentLength;
EndSegmentLength = Parent.EndSegmentLength;
_extraTimelines = new List<Timeline>(); OnPropertyChanged(nameof(Length));
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines); OnTimelineChanged();
}
/// <summary>
/// Occurs when changes have been made to any of the segments of the timeline.
/// </summary>
public event EventHandler? TimelineChanged;
private void OnTimelineChanged()
{
TimelineChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#endregion
#region Updating
private TimeSpan _lastOverridePosition;
/// <summary>
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
/// </summary>
/// <param name="delta">The amount of time to apply to the position</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (_lock)
{
Delta += delta;
Position += delta;
IsOverridden = false;
_lastOverridePosition = Position;
if (stickToMainSegment && Position > MainSegmentEndPosition)
{
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
_extraTimelines.RemoveAll(t => t.IsFinished);
foreach (Timeline extraTimeline in _extraTimelines)
extraTimeline.Update(delta, false);
} }
}
/// <inheritdoc /> /// <summary>
public override string ToString() /// Moves the position of the timeline backwards to the very start of the timeline
/// </summary>
public void JumpToStart()
{
lock (_lock)
{ {
return $"Progress: {Position}/{Length} - delta: {Delta}"; if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
} }
}
#region Extra timelines /// <summary>
/// Moves the position of the timeline forwards to the beginning of the end segment
/// <summary> /// </summary>
/// Adds an extra timeline to this timeline public void JumpToEndSegment()
/// </summary> {
public void AddExtraTimeline() lock (_lock)
{ {
_extraTimelines.Add(new Timeline(this)); if (Position >= EndSegmentStartPosition)
if (_extraTimelines.Count > MaxExtraTimelines) return;
_extraTimelines.RemoveAt(0);
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
} }
}
/// <summary> /// <summary>
/// Removes all extra timelines from this timeline /// Moves the position of the timeline forwards to the very end of the timeline
/// </summary> /// </summary>
public void ClearExtraTimelines() public void JumpToEnd()
{
lock (_lock)
{ {
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
}
}
/// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment)
{
lock (_lock)
{
Delta += position - _lastOverridePosition;
Position = position;
IsOverridden = true;
_lastOverridePosition = position;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
{
bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
{
Position = MainSegmentStartPosition;
}
}
_extraTimelines.Clear(); _extraTimelines.Clear();
} }
#endregion
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary>
/// Gets the current position of the timeline
/// </summary>
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
/// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
/// <see cref="ClearDelta" />
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary>
public TimeSpan Delta
{
get => Parent == null ? _lastDelta : DeltaToParent;
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <summary>
/// Gets a list of extra copies of the timeline applied to this timeline
/// </summary>
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
/// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden
/// </summary>
public bool IsOverridden { get; private set; }
#region Segments
/// <summary>
/// Gets the total length of this timeline
/// </summary>
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
if (SetAndNotify(ref _startSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
/// <summary>
/// Gets or sets the start position of the main segment
/// </summary>
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
/// <summary>
/// Gets or sets the end position of the start segment
/// </summary>
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the end position of the main segment
/// </summary>
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
/// <summary>
/// Notifies the right segments in a way that I don't have to think about it
/// </summary>
/// <param name="segment">The segment that was updated</param>
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
OnPropertyChanged(nameof(EndSegmentStartPosition));
OnPropertyChanged(nameof(EndSegmentEndPosition));
}
if (segment <= TimelineSegment.Main)
{
if (startUpdated || segment < TimelineSegment.Main)
OnPropertyChanged(nameof(MainSegmentStartPosition));
OnPropertyChanged(nameof(MainSegmentEndPosition));
}
if (segment <= TimelineSegment.Start) OnPropertyChanged(nameof(StartSegmentEndPosition));
OnPropertyChanged(nameof(Length));
}
#endregion
#endregion
#region Updating
private TimeSpan _lastOverridePosition;
/// <summary>
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
/// </summary>
/// <param name="delta">The amount of time to apply to the position</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (_lock)
{
Delta += delta;
Position += delta;
IsOverridden = false;
_lastOverridePosition = Position;
if (stickToMainSegment && Position > MainSegmentEndPosition)
{
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
_extraTimelines.RemoveAll(t => t.IsFinished);
foreach (Timeline extraTimeline in _extraTimelines)
extraTimeline.Update(delta, false);
}
}
/// <summary>
/// Moves the position of the timeline backwards to the very start of the timeline
/// </summary>
public void JumpToStart()
{
lock (_lock)
{
if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
}
}
/// <summary>
/// Moves the position of the timeline forwards to the beginning of the end segment
/// </summary>
public void JumpToEndSegment()
{
lock (_lock)
{
if (Position >= EndSegmentStartPosition)
return;
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
}
}
/// <summary>
/// Moves the position of the timeline forwards to the very end of the timeline
/// </summary>
public void JumpToEnd()
{
lock (_lock)
{
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
}
}
/// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment)
{
lock (_lock)
{
Delta += position - _lastOverridePosition;
Position = position;
IsOverridden = true;
_lastOverridePosition = position;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
{
bool atSegmentStart = Position == MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
Position = MainSegmentStartPosition;
}
_extraTimelines.Clear();
}
}
/// <summary>
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
/// </summary>
public void ClearDelta()
{
lock (_lock)
{
Delta = TimeSpan.Zero;
}
}
#endregion
#region Storage
/// <inheritdoc />
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
/// <inheritdoc />
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
}
internal enum TimelineSegment
{
Start,
Main,
End
} }
/// <summary> /// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met /// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
/// </summary> /// </summary>
public enum TimelinePlayMode public void ClearDelta()
{ {
/// <summary> lock (_lock)
/// Continue repeating the main segment of the timeline while the condition is met {
/// </summary> Delta = TimeSpan.Zero;
Repeat, }
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
} }
#endregion
#region Storage
/// <inheritdoc />
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
/// <inheritdoc />
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
}
internal enum TimelineSegment
{
Start,
Main,
End
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum TimelinePlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary> /// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met /// Only play the timeline once when the condition is met
/// </summary> /// </summary>
public enum TimelineStopMode Once
{ }
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary> /// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline /// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary> /// </summary>
SkipToEnd public enum TimelineStopMode
} {
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary> /// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired /// When conditions are no longer met, skip to the end segment of the timeline
/// </summary> /// </summary>
public enum TimeLineEventOverlapMode SkipToEnd
{ }
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary> /// <summary>
/// Ignore subsequent event fires until the timeline finishes /// Represents a mode for render elements to start their timeline when display conditions events are fired
/// </summary> /// </summary>
Ignore, public enum TimeLineEventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary> /// <summary>
/// Play another copy of the timeline on top of the current run /// Ignore subsequent event fires until the timeline finishes
/// </summary> /// </summary>
Copy, Ignore,
/// <summary> /// <summary>
/// Repeat the timeline until the event fires again /// Play another copy of the timeline on top of the current run
/// </summary> /// </summary>
Toggle Copy,
}
/// <summary>
/// Repeat the timeline until the event fires again
/// </summary>
Toggle
} }

View File

@ -337,7 +337,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{ {
if (end < TimeSpan.FromMilliseconds(100)) if (end < TimeSpan.FromMilliseconds(100))
end = TimeSpan.FromMilliseconds(100); end = TimeSpan.FromMilliseconds(100);
if (Segment == SegmentViewModelType.Start) if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.Timeline.StartSegmentEndPosition = end; SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
else if (Segment == SegmentViewModelType.Main) else if (Segment == SegmentViewModelType.Main)

View File

@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary> /// <summary>
/// Represents a profile editor command that can be used to delete a keyframe. /// Represents a profile editor command that can be used to delete a keyframe.
/// </summary> /// </summary>
public class DeleteKeyframe<T> : IProfileEditorCommand public class DeleteKeyframe : IProfileEditorCommand
{ {
private readonly LayerPropertyKeyframe<T> _keyframe; private readonly ILayerPropertyKeyframe _keyframe;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class. /// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
/// </summary> /// </summary>
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe) public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
{ {
_keyframe = keyframe; _keyframe = keyframe;
} }
@ -25,13 +25,13 @@ public class DeleteKeyframe<T> : IProfileEditorCommand
/// <inheritdoc /> /// <inheritdoc />
public void Execute() public void Execute()
{ {
_keyframe.LayerProperty.RemoveKeyframe(_keyframe); _keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Undo() public void Undo()
{ {
_keyframe.LayerProperty.AddKeyframe(_keyframe); _keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
} }
#endregion #endregion

View File

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

View File

@ -12,6 +12,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
private readonly T _newValue; private readonly T _newValue;
private readonly T _originalValue; private readonly T _originalValue;
private readonly TimeSpan? _time; private readonly TimeSpan? _time;
private LayerPropertyKeyframe<T>? _newKeyframe;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class. /// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
@ -43,13 +44,16 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
/// <inheritdoc /> /// <inheritdoc />
public void Execute() public void Execute()
{ {
_layerProperty.SetCurrentValue(_newValue, _time); _newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Undo() public void Undo()
{ {
_layerProperty.SetCurrentValue(_originalValue, _time); if (_newKeyframe != null)
_layerProperty.RemoveKeyframe(_newKeyframe);
else
_layerProperty.SetCurrentValue(_originalValue, _time);
} }
#endregion #endregion

View File

@ -4,6 +4,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>

View File

@ -8,6 +8,7 @@ using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties; using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar;
@ -73,14 +74,9 @@ namespace Artemis.UI.Ninject.Factories
PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels); TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
} }
public interface IPropertyVmFactory public interface IPropertyVmFactory

View File

@ -6,6 +6,9 @@
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView"> x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid"> <Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
<Grid RowDefinitions="48,*"> <Grid RowDefinitions="48,*">
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" /> <ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
@ -31,76 +34,103 @@
Background="Transparent" Background="Transparent"
Margin="0 0 -5 0" /> Margin="0 0 -5 0" />
<Grid Grid.Column="2" RowDefinitions="48,*"> <!-- Horizontal scrolling -->
<!-- Timeline header body --> <ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<controls:TimelineHeader Grid.Row="0" <Grid RowDefinitions="48,*">
Name="TimelineHeader" <!-- Timeline header body -->
Margin="0 18 0 0" <controls:TimelineHeader Grid.Row="0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" Name="TimelineHeader"
PixelsPerSecond="{Binding PixelsPerSecond}" Margin="0 18 0 0"
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}" HorizontalAlignment="Left"
OffsetFirstValue="True" PixelsPerSecond="{Binding PixelsPerSecond}"
PointerReleased="TimelineHeader_OnPointerReleased" HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
Width="{Binding #TimelineScrollViewer.Viewport.Width}" VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
Cursor="Hand" /> OffsetFirstValue="True"
PointerReleased="TimelineHeader_OnPointerReleased"
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
Cursor="Hand" />
<Canvas Grid.Row="0" ZIndex="2"> <Canvas Grid.Row="0" ZIndex="2">
<!-- Timeline segments --> <!-- Segment dividers -->
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" /> <Line Name="TimelineLine"
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" /> Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" /> Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
StartPoint="0,0"
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}">
</Line>
<!-- Timeline caret --> <Line Name="StartSegmentLine"
<Polygon Name="TimelineCaret" Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.EndX}"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}" IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
Cursor="SizeWestEast" StartPoint="0,0"
PointerPressed="TimelineCaret_OnPointerPressed" EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
PointerReleased="TimelineCaret_OnPointerReleased" StrokeThickness="2"
PointerMoved="TimelineCaret_OnPointerMoved" Stroke="{DynamicResource SystemAccentColorLight1}"
Points="-8,0 -8,8 0,20, 8,8 8,0" StrokeDashArray="6,2"
Fill="{DynamicResource SystemAccentColorLight1}"> Opacity="0.5">
<!-- <Polygon.Transitions> --> </Line>
<!-- <Transitions> --> <Line Name="MainSegmentLine"
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> --> Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.EndX}"
<!-- </Transitions> --> IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
<!-- </Polygon.Transitions> --> StartPoint="0,0"
</Polygon> EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
<Line Name="TimelineLine" StrokeThickness="2"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}" Stroke="{DynamicResource SystemAccentColorLight1}"
Cursor="SizeWestEast" StrokeDashArray="6,2"
PointerPressed="TimelineCaret_OnPointerPressed" Opacity="0.5">
PointerReleased="TimelineCaret_OnPointerReleased" </Line>
PointerMoved="TimelineCaret_OnPointerMoved" <Line Name="EndSegmentLine"
StartPoint="0,0" Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.EndX}"
EndPoint="0,1" IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
StrokeThickness="2" StartPoint="0,0"
Stroke="{DynamicResource SystemAccentColorLight1}" EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
RenderTransformOrigin="0,0"> StrokeThickness="2"
<!-- <Line.Transitions> --> Stroke="{DynamicResource SystemAccentColorLight1}"
<!-- <Transitions> --> StrokeDashArray="6,2"
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> --> Opacity="0.5">
<!-- </Transitions> --> </Line>
<!-- </Line.Transitions> -->
<Line.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}" />
</Line.RenderTransform>
</Line>
</Canvas>
<!-- Horizontal scrolling --> <!-- Timeline segments -->
<ScrollViewer Grid.Row="1" <ContentControl Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.StartX}"
Name="TimelineScrollViewer" Classes="segment-content-control"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}" Content="{Binding TimelineViewModel.EndSegmentViewModel}" />
VerticalScrollBarVisibility="Hidden" <ContentControl Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.StartX}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"> Classes="segment-content-control"
<ContentControl Content="{Binding TimelineViewModel}" /> Content="{Binding TimelineViewModel.MainSegmentViewModel}" />
</ScrollViewer> <ContentControl Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{Binding TimelineViewModel.StartSegmentViewModel}" />
<!-- TODO: Databindings here --> <!-- Timeline caret -->
<Polygon Name="TimelineCaret"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}">
</Polygon>
</Canvas>
</Grid> <ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden">
<ContentControl Content="{Binding TimelineViewModel}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
</ScrollViewer>
<!-- TODO: Databindings here -->
</Grid>
</ScrollViewer>
</Grid> </Grid>

View File

@ -8,6 +8,7 @@ using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput; using Artemis.UI.Shared.Services.PropertyInput;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;

View File

@ -1,8 +1,7 @@
using System; using System;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public interface ITimelineKeyframeViewModel public interface ITimelineKeyframeViewModel
{ {

View File

@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineEasingView"> x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}" <Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1" StrokeThickness="1"

View File

@ -1,7 +1,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
{ {
public partial class TimelineEasingView : UserControl public partial class TimelineEasingView : UserControl
{ {

View File

@ -4,7 +4,7 @@ using Artemis.UI.Shared;
using Avalonia; using Avalonia;
using Humanizer; using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineEasingViewModel : ViewModelBase public class TimelineEasingViewModel : ViewModelBase
{ {

View File

@ -5,7 +5,7 @@
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline" xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineKeyframeView" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
ClipToBounds="False" ClipToBounds="False"
Height="{DynamicResource RailsHeight}"> Height="{DynamicResource RailsHeight}">
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}" <Ellipse Fill="{DynamicResource SystemAccentColorLight2}"

View File

@ -4,7 +4,7 @@ using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel> public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{ {

View File

@ -7,11 +7,10 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData; using DynamicData;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{ {
@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
public void Delete() public void Delete()
{ {
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe)); _profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
} }
#endregion #endregion

View File

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

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -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)
{
}
}
}

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

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

View File

@ -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();
}
}

View File

@ -12,8 +12,8 @@
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<ItemsControl.Styles> <ItemsControl.Styles>
<Style Selector="ContentPresenter"> <Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
</Style> </Style>
</ItemsControl.Styles> </ItemsControl.Styles>
</ItemsControl> </ItemsControl>

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
@ -24,6 +25,9 @@ public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelineP
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x) Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(_ => UpdateKeyframes()) .Subscribe(_ => UpdateKeyframes())
.DisposeWith(d); .DisposeWith(d);

View File

@ -10,8 +10,11 @@
<x:Double x:Key="RailsHeight">28</x:Double> <x:Double x:Key="RailsHeight">28</x:Double>
<x:Double x:Key="RailsBorderHeight">29</x:Double> <x:Double x:Key="RailsBorderHeight">29</x:Double>
</UserControl.Resources> </UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased"> <Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0"> <Grid.KeyBindings>
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
</Grid.KeyBindings>
<ItemsControl Items="{Binding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}"> <TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TimelineGroupViewModel}" /> <ContentControl Content="{Binding TimelineGroupViewModel}" />

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared.Controls; using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Extensions;

View File

@ -3,6 +3,9 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.ProfileEditor.Commands;
@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<double>? _caretPosition; private ObservableAsPropertyHelper<double>? _caretPosition;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond; private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private List<ITimelineKeyframeViewModel>? _moveKeyframes; private List<ITimelineKeyframeViewModel>? _moveKeyframes;
private ObservableAsPropertyHelper<double> _minWidth;
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService) public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
StartSegmentViewModel startSegmentViewModel,
MainSegmentViewModel mainSegmentViewModel,
EndSegmentViewModel endSegmentViewModel,
IProfileEditorService profileEditorService)
{ {
PropertyGroupViewModels = propertyGroupViewModels; PropertyGroupViewModels = propertyGroupViewModels;
StartSegmentViewModel = startSegmentViewModel;
MainSegmentViewModel = mainSegmentViewModel;
EndSegmentViewModel = endSegmentViewModel;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
this.WhenActivated(d => this.WhenActivated(d =>
@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p) .CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition) .ToProperty(this, vm => vm.CaretPosition)
.DisposeWith(d); .DisposeWith(d);
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); _pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
_minWidth = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
.ToProperty(this, vm => vm.MinWidth)
.DisposeWith(d);
}); });
} }
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; } public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public StartSegmentViewModel StartSegmentViewModel { get; }
public MainSegmentViewModel MainSegmentViewModel { get; }
public EndSegmentViewModel EndSegmentViewModel { get; }
public double CaretPosition => _caretPosition?.Value ?? 0.0; public double CaretPosition => _caretPosition?.Value ?? 0.0;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public double MinWidth => _minWidth?.Value ?? 0;
public void ChangeTime(TimeSpan newTime) public void ChangeTime(TimeSpan newTime)
{ {
@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase
#region Keyframe actions #region Keyframe actions
public void DuplicateKeyframes(ITimelineKeyframeViewModel source) public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
{ {
if (!source.IsSelected) if (source is { IsSelected: false })
source.Duplicate(); source.Delete();
else else
{ {
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList(); List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
@ -131,9 +152,9 @@ public class TimelineViewModel : ActivatableViewModelBase
} }
} }
public void CopyKeyframes(ITimelineKeyframeViewModel source) public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
{ {
if (!source.IsSelected) if (source is { IsSelected: false })
source.Copy(); source.Copy();
else else
{ {
@ -144,9 +165,9 @@ public class TimelineViewModel : ActivatableViewModelBase
} }
} }
public void PasteKeyframes(ITimelineKeyframeViewModel source) public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
{ {
if (!source.IsSelected) if (source is { IsSelected: false })
source.Paste(); source.Paste();
else else
{ {
@ -157,9 +178,9 @@ public class TimelineViewModel : ActivatableViewModelBase
} }
} }
public void DeleteKeyframes(ITimelineKeyframeViewModel source) public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
{ {
if (!source.IsSelected) if (source is {IsSelected: false})
source.Delete(); source.Delete();
else else
{ {