diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs index bcf182c27..be7528402 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs @@ -14,12 +14,12 @@ namespace Artemis.Core ILayerProperty UntypedLayerProperty { get; } /// - /// The position of this keyframe in the timeline + /// Gets or sets the position of this keyframe in the timeline /// TimeSpan Position { get; set; } /// - /// The easing function applied on the value of the keyframe + /// Gets or sets the easing function applied on the value of the keyframe /// Easings.Functions EasingFunction { get; set; } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeKeyframeEasing.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeKeyframeEasing.cs new file mode 100644 index 000000000..d7e2ca932 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeKeyframeEasing.cs @@ -0,0 +1,43 @@ +using Artemis.Core; +using Humanizer; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to change the easing function of a keyframe. +/// +public class ChangeKeyframeEasing : IProfileEditorCommand +{ + private readonly ILayerPropertyKeyframe _keyframe; + private readonly Easings.Functions _easingFunction; + private readonly Easings.Functions _originalEasingFunction; + + /// + /// Creates a new instance of the class. + /// + public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction) + { + _keyframe = keyframe; + _easingFunction = easingFunction; + _originalEasingFunction = keyframe.EasingFunction; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase); + + /// + public void Execute() + { + _keyframe.EasingFunction = _easingFunction; + } + + /// + public void Undo() + { + _keyframe.EasingFunction = _originalEasingFunction; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs new file mode 100644 index 000000000..790cf229d --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs @@ -0,0 +1,43 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to change the position of a keyframe. +/// +public class MoveKeyframe : IProfileEditorCommand +{ + private readonly ILayerPropertyKeyframe _keyframe; + private readonly TimeSpan _originalPosition; + private readonly TimeSpan _position; + + /// + /// Creates a new instance of the class. + /// + public MoveKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position) + { + _keyframe = keyframe; + _position = position; + _originalPosition = keyframe.Position; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Move keyframe"; + + /// + public void Execute() + { + _keyframe.Position = _position; + } + + /// + public void Undo() + { + _keyframe.Position = _originalPosition; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs new file mode 100644 index 000000000..e2a2eb4e4 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Represents a profile editor command that can be used to combine multiple commands into one. +/// +public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable +{ + private readonly List _commands; + + /// + /// Creates a new instance of the class. + /// + /// The commands to execute. + /// The display name of the composite command. + public CompositeProfileEditorCommand(IEnumerable commands, string displayName) + { + if (commands == null) + throw new ArgumentNullException(nameof(commands)); + _commands = commands.ToList(); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + + /// + public void Dispose() + { + foreach (IProfileEditorCommand profileEditorCommand in _commands) + if (profileEditorCommand is IDisposable disposable) + disposable.Dispose(); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + foreach (IProfileEditorCommand profileEditorCommand in _commands) + profileEditorCommand.Execute(); + } + + /// + public void Undo() + { + // Undo in reverse by iterating from the back + for (int index = _commands.Count; index >= 0; index--) + _commands[index].Undo(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs index d91a2d9a5..e90f1a699 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs @@ -25,7 +25,6 @@ public interface ITimelineKeyframeViewModel #region Context menu actions void PopulateEasingViewModels(); - void ClearEasingViewModels(); void Delete(bool save = true); #endregion diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml new file mode 100644 index 000000000..a5672ee0e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml.cs new file mode 100644 index 000000000..ea41308db --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline +{ + public partial class TimelineEasingView : UserControl + { + public TimelineEasingView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingViewModel.cs index d43aaf9c6..2a43c5b75 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineEasingViewModel.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Artemis.Core; using Artemis.UI.Shared; using Avalonia; @@ -9,11 +8,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; public class TimelineEasingViewModel : ViewModelBase { - private bool _isEasingModeSelected; + private readonly ILayerPropertyKeyframe _keyframe; - public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected) + public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe) { - _isEasingModeSelected = isSelected; + _keyframe = keyframe; EasingFunction = easingFunction; Description = easingFunction.Humanize(); @@ -30,21 +29,5 @@ public class TimelineEasingViewModel : ViewModelBase public Easings.Functions EasingFunction { get; } public List EasingPoints { get; } public string Description { get; } - - public bool IsEasingModeSelected - { - get => _isEasingModeSelected; - set - { - _isEasingModeSelected = value; - if (value) OnEasingModeSelected(); - } - } - - public event EventHandler EasingModeSelected; - - protected virtual void OnEasingModeSelected() - { - EasingModeSelected?.Invoke(this, EventArgs.Empty); - } + public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction; } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml index 6bcd67db5..a9f501b03 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml @@ -3,6 +3,7 @@ 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" + xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineKeyframeView" ClipToBounds="False" @@ -31,25 +32,24 @@ - + + + + - - - - - - - - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs index 42b7d25c7..c71564082 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -43,4 +44,9 @@ public class TimelineKeyframeView : ReactiveUserControl : ActivatableViewModelBase, ITimelineK { _pixelsPerSecond = p; profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d); - System.Reactive.Disposables.Disposable.Create(() => - { - foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) - timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected; - }).DisposeWith(d); }).DisposeWith(d); _isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d); @@ -142,31 +139,45 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)) .Cast() - .Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction))); - - foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) - timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected; + .Select(e => new TimelineEasingViewModel(e, Keyframe))); } - public void ClearEasingViewModels() + public void SelectEasingFunction(Easings.Functions easingFunction) { - EasingViewModels.Clear(); + _profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction)); } - private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e) + #endregion +} + +public class ChangeKeyframeEasing : IProfileEditorCommand +{ + private readonly ILayerPropertyKeyframe _keyframe; + private readonly Easings.Functions _easingFunction; + private readonly Easings.Functions _originalEasingFunction; + + public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction) { - if (sender is TimelineEasingViewModel timelineEasingViewModel) - SelectEasingMode(timelineEasingViewModel); + _keyframe = keyframe; + _easingFunction = easingFunction; + _originalEasingFunction = keyframe.EasingFunction; } - public void SelectEasingMode(TimelineEasingViewModel easingViewModel) - { - throw new NotImplementedException(); + #region Implementation of IProfileEditorCommand - LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction; - // Set every selection to false except on the VM that made the change - foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) - propertyTrackEasingViewModel.IsEasingModeSelected = false; + /// + public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase); + + /// + public void Execute() + { + _keyframe.EasingFunction = _easingFunction; + } + + /// + public void Undo() + { + _keyframe.EasingFunction = _originalEasingFunction; } #endregion