From 642823add5db200b3c056a2acaf5f209315d45a2 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 25 Jan 2022 00:23:26 +0100 Subject: [PATCH] Timeline - Implemented segments --- .../Profile/LayerProperties/ILayerProperty.cs | 18 +- .../Profile/LayerProperties/LayerProperty.cs | 31 +- src/Artemis.Core/Models/Profile/Timeline.cs | 952 +++++++++--------- .../Timeline/TimelineSegmentViewModel.cs | 2 +- .../ProfileEditor/Commands/DeleteKeyframe.cs | 10 +- .../Commands/ResizeTimelineSegment.cs | 108 ++ .../Commands/UpdateLayerProperty.cs | 8 +- .../Artemis.UI/Artemis.UI.csproj.DotSettings | 1 + .../Ninject/Factories/IVMFactory.cs | 8 +- .../Panels/Properties/PropertiesView.axaml | 158 +-- .../Properties/PropertyGroupViewModel.cs | 1 + .../Timeline/ITimelinePropertyViewModel.cs | 1 + .../ITimelineKeyframeViewModel.cs | 3 +- .../{ => Keyframes}/TimelineEasingView.axaml | 2 +- .../TimelineEasingView.axaml.cs | 2 +- .../TimelineEasingViewModel.cs | 2 +- .../TimelineKeyframeView.axaml | 2 +- .../TimelineKeyframeView.axaml.cs | 2 +- .../TimelineKeyframeViewModel.cs | 5 +- .../Timeline/Segments/EndSegmentView.axaml | 56 ++ .../Timeline/Segments/EndSegmentView.axaml.cs | 50 + .../Timeline/Segments/EndSegmentViewModel.cs | 76 ++ .../Timeline/Segments/MainSegmentView.axaml | 78 ++ .../Segments/MainSegmentView.axaml.cs | 62 ++ .../Timeline/Segments/MainSegmentViewModel.cs | 76 ++ .../Timeline/Segments/Segment.axaml | 28 + .../Timeline/Segments/StartSegmentView.axaml | 54 + .../Segments/StartSegmentView.axaml.cs | 50 + .../Segments/StartSegmentViewModel.cs | 69 ++ .../Segments/TimelineSegmentViewModel.cs | 174 ++++ .../Timeline/TimelinePropertyView.axaml | 4 +- .../Timeline/TimelinePropertyViewModel.cs | 4 + .../Properties/Timeline/TimelineView.axaml | 7 +- .../Properties/Timeline/TimelineView.axaml.cs | 1 + .../Properties/Timeline/TimelineViewModel.cs | 43 +- 35 files changed, 1569 insertions(+), 579 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResizeTimelineSegment.cs rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/ITimelineKeyframeViewModel.cs (83%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineEasingView.axaml (95%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineEasingView.axaml.cs (81%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineEasingViewModel.cs (92%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineKeyframeView.axaml (99%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineKeyframeView.axaml.cs (97%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/{ => Keyframes}/TimelineKeyframeViewModel.cs (96%) create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs 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 {