From 913117ad0a42b68e698fd68e0a9edfeaf6e27811 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 22 Jan 2022 01:19:10 +0100 Subject: [PATCH] Profile editor - Implemented keyframe selection, movement, deletion Profile editor - Added command scoping --- .../Profile/LayerProperties/LayerProperty.cs | 5 +- .../LayerProperties/LayerPropertyKeyFrame.cs | 1 + .../CompositeCommand.cs} | 33 ++++- .../ProfileEditor/Commands/DeleteKeyframe.cs | 38 +++++ .../ProfileEditor/Commands/MoveKeyframe.cs | 10 ++ .../ProfileEditor/IProfileEditorService.cs | 35 ++++- .../ProfileEditorCommandScope.cs | 44 ++++++ .../ProfileEditor/ProfileEditorHistory.cs | 139 +++++++++--------- .../ProfileEditor/ProfileEditorService.cs | 58 ++++++++ .../Panels/Properties/PropertiesView.axaml.cs | 2 +- .../Timeline/ITimelineKeyframeViewModel.cs | 17 ++- .../Timeline/TimelineKeyframeView.axaml | 11 +- .../Timeline/TimelineKeyframeView.axaml.cs | 37 ++++- .../Timeline/TimelineKeyframeViewModel.cs | 129 +++++++--------- .../Properties/Timeline/TimelineViewModel.cs | 128 +++++++++++++++- 15 files changed, 515 insertions(+), 172 deletions(-) rename src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/{CompositeProfileEditorCommand.cs => Commands/CompositeCommand.cs} (51%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index c2f46fad2..981805482 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -213,7 +213,7 @@ namespace Artemis.Core SetCurrentValue(CoreJson.DeserializeObject(json)!, null); } - private void ReapplyUpdate() + internal void ReapplyUpdate() { // Create a timeline with the same position but a delta of zero Timeline temporaryTimeline = new(); @@ -291,6 +291,7 @@ namespace Artemis.Core KeyframesEnabled = true; SortKeyframes(); + ReapplyUpdate(); OnKeyframeAdded(); } @@ -323,7 +324,9 @@ namespace Artemis.Core return; _keyframes.Remove(keyframe); + SortKeyframes(); + ReapplyUpdate(); OnKeyframeRemoved(); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index 299c0638e..1f26a1876 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -57,6 +57,7 @@ namespace Artemis.Core { SetAndNotify(ref _position, value); LayerProperty.SortKeyframes(); + LayerProperty.ReapplyUpdate(); } } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs similarity index 51% rename from src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs rename to src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs index e2a2eb4e4..9dea007c7 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs @@ -2,21 +2,22 @@ using System.Collections.Generic; using System.Linq; -namespace Artemis.UI.Shared.Services.ProfileEditor; +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; /// /// Represents a profile editor command that can be used to combine multiple commands into one. /// -public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable +public class CompositeCommand : IProfileEditorCommand, IDisposable { + private bool _ignoreNextExecute; private readonly List _commands; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The commands to execute. /// The display name of the composite command. - public CompositeProfileEditorCommand(IEnumerable commands, string displayName) + public CompositeCommand(IEnumerable commands, string displayName) { if (commands == null) throw new ArgumentNullException(nameof(commands)); @@ -24,6 +25,22 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); } + /// + /// Creates a new instance of the class. + /// + /// The commands to execute. + /// The display name of the composite command. + /// Whether or not to ignore the first execute because commands are already executed + internal CompositeCommand(IEnumerable commands, string displayName, bool ignoreFirstExecute) + { + if (commands == null) + throw new ArgumentNullException(nameof(commands)); + + _ignoreNextExecute = ignoreFirstExecute; + _commands = commands.ToList(); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + /// public void Dispose() { @@ -40,6 +57,12 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable /// public void Execute() { + if (_ignoreNextExecute) + { + _ignoreNextExecute = false; + return; + } + foreach (IProfileEditorCommand profileEditorCommand in _commands) profileEditorCommand.Execute(); } @@ -48,7 +71,7 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable public void Undo() { // Undo in reverse by iterating from the back - for (int index = _commands.Count; index >= 0; index--) + for (int index = _commands.Count - 1; index >= 0; index--) _commands[index].Undo(); } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs new file mode 100644 index 000000000..4f6b552bb --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs @@ -0,0 +1,38 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to delete a keyframe. +/// +public class DeleteKeyframe : IProfileEditorCommand +{ + private readonly LayerPropertyKeyframe _keyframe; + + /// + /// Creates a new instance of the class. + /// + public DeleteKeyframe(LayerPropertyKeyframe keyframe) + { + _keyframe = keyframe; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Delete keyframe"; + + /// + public void Execute() + { + _keyframe.LayerProperty.RemoveKeyframe(_keyframe); + } + + /// + public void Undo() + { + _keyframe.LayerProperty.AddKeyframe(_keyframe); + } + + #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 index 790cf229d..26faf2bfc 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs @@ -22,6 +22,16 @@ public class MoveKeyframe : IProfileEditorCommand _originalPosition = keyframe.Position; } + /// + /// Creates a new instance of the class. + /// + public MoveKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position, TimeSpan originalPosition) + { + _keyframe = keyframe; + _position = position; + _originalPosition = originalPosition; + } + #region Implementation of IProfileEditorCommand /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 9ce0edf0b..7be4f2843 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -43,7 +43,7 @@ public interface IProfileEditorService : IArtemisSharedUIService IObservable PixelsPerSecond { get; } /// - /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. + /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. /// /// An observable which emits the change set. IObservable> ConnectToKeyframes(); @@ -73,18 +73,27 @@ public interface IProfileEditorService : IArtemisSharedUIService void ChangePixelsPerSecond(int pixelsPerSecond); /// - /// Selects the provided keyframe. + /// Selects the provided keyframe. /// /// The keyframe to select. - /// If expands the current selection; otherwise replaces it with only the provided . - /// If toggles the selection and only for the provided . + /// + /// If expands the current selection; otherwise replaces it with only the + /// provided . + /// + /// + /// If toggles the selection and only for the provided + /// . + /// void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle); /// - /// Selects the provided keyframes. + /// Selects the provided keyframes. /// /// The keyframes to select. - /// If expands the current selection; otherwise replaces it with only the provided . + /// + /// If expands the current selection; otherwise replaces it with only the + /// provided . + /// void SelectKeyframes(IEnumerable keyframes, bool expand); /// @@ -99,12 +108,26 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The snapped time. TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List? snapTimes = null); + /// + /// Rounds the given time to something appropriate for the current zoom level. + /// + /// The time to round + /// The rounded time. + TimeSpan RoundTime(TimeSpan time); + /// /// Executes the provided command and adds it to the history. /// /// The command to execute. void ExecuteCommand(IProfileEditorCommand command); + /// + /// Creates a new command scope which can be used to group undo/redo actions of multiple commands. + /// + /// The name of the command scope. + /// The command scope that will group any commands until disposed. + ProfileEditorCommandScope CreateCommandScope(string name); + /// /// Saves the current profile. /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs new file mode 100644 index 000000000..aa760b121 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Represents a scope in which editor commands are executed until disposed. +/// +public class ProfileEditorCommandScope : IDisposable +{ + private readonly List _commands; + + private readonly ProfileEditorService _profileEditorService; + + internal ProfileEditorCommandScope(ProfileEditorService profileEditorService, string name) + { + Name = name; + _profileEditorService = profileEditorService; + _commands = new List(); + } + + /// + /// Gets the name of the scope. + /// + public string Name { get; } + + /// + /// Gets a read only collection of commands in the scope. + /// + public ReadOnlyCollection ProfileEditorCommands => new(_commands); + + internal void AddCommand(IProfileEditorCommand command) + { + command.Execute(); + _commands.Add(command); + } + + /// + public void Dispose() + { + _profileEditorService.StopCommandScope(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs index f09f471f4..109714044 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs @@ -7,94 +7,93 @@ using System.Reactive.Subjects; using Artemis.Core; using ReactiveUI; -namespace Artemis.UI.Shared.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor; + +public class ProfileEditorHistory { - public class ProfileEditorHistory + private readonly Subject _canRedo = new(); + private readonly Subject _canUndo = new(); + private readonly Stack _redoCommands = new(); + private readonly Stack _undoCommands = new(); + + public ProfileEditorHistory(ProfileConfiguration profileConfiguration) { - private readonly Subject _canRedo = new(); - private readonly Subject _canUndo = new(); - private readonly Stack _redoCommands = new(); - private readonly Stack _undoCommands = new(); + ProfileConfiguration = profileConfiguration; - public ProfileEditorHistory(ProfileConfiguration profileConfiguration) - { - ProfileConfiguration = profileConfiguration; + Execute = ReactiveCommand.Create(ExecuteEditorCommand); + Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo); + Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo); + } - Execute = ReactiveCommand.Create(ExecuteEditorCommand); - Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo); - Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo); - } + public ProfileConfiguration ProfileConfiguration { get; } + public IObservable CanUndo => _canUndo.AsObservable().DistinctUntilChanged(); + public IObservable CanRedo => _canRedo.AsObservable().DistinctUntilChanged(); - public ProfileConfiguration ProfileConfiguration { get; } - public IObservable CanUndo => _canUndo.AsObservable().DistinctUntilChanged(); - public IObservable CanRedo => _canRedo.AsObservable().DistinctUntilChanged(); + public ReactiveCommand Execute { get; } + public ReactiveCommand Undo { get; } + public ReactiveCommand Redo { get; } - public ReactiveCommand Execute { get; } - public ReactiveCommand Undo { get; } - public ReactiveCommand Redo { get; } + public void Clear() + { + ClearRedo(); + ClearUndo(); + UpdateSubjects(); + } - public void Clear() - { - ClearRedo(); - ClearUndo(); - UpdateSubjects(); - } + public void ExecuteEditorCommand(IProfileEditorCommand command) + { + command.Execute(); - public void ExecuteEditorCommand(IProfileEditorCommand command) - { - command.Execute(); + _undoCommands.Push(command); + ClearRedo(); + UpdateSubjects(); + } - _undoCommands.Push(command); - ClearRedo(); - UpdateSubjects(); - } + private void ClearRedo() + { + foreach (IProfileEditorCommand profileEditorCommand in _redoCommands) + if (profileEditorCommand is IDisposable disposable) + disposable.Dispose(); - private void ClearRedo() - { - foreach (IProfileEditorCommand profileEditorCommand in _redoCommands) - if (profileEditorCommand is IDisposable disposable) - disposable.Dispose(); + _redoCommands.Clear(); + } - _redoCommands.Clear(); - } + private void ClearUndo() + { + foreach (IProfileEditorCommand profileEditorCommand in _undoCommands) + if (profileEditorCommand is IDisposable disposable) + disposable.Dispose(); - private void ClearUndo() - { - foreach (IProfileEditorCommand profileEditorCommand in _undoCommands) - if (profileEditorCommand is IDisposable disposable) - disposable.Dispose(); + _undoCommands.Clear(); + } - _undoCommands.Clear(); - } + private IProfileEditorCommand? ExecuteUndo() + { + if (!_undoCommands.TryPop(out IProfileEditorCommand? command)) + return null; - private IProfileEditorCommand? ExecuteUndo() - { - if (!_undoCommands.TryPop(out IProfileEditorCommand? command)) - return null; + command.Undo(); + _redoCommands.Push(command); + UpdateSubjects(); - command.Undo(); - _redoCommands.Push(command); - UpdateSubjects(); + return command; + } - return command; - } + private IProfileEditorCommand? ExecuteRedo() + { + if (!_redoCommands.TryPop(out IProfileEditorCommand? command)) + return null; - private IProfileEditorCommand? ExecuteRedo() - { - if (!_redoCommands.TryPop(out IProfileEditorCommand? command)) - return null; + command.Execute(); + _undoCommands.Push(command); + UpdateSubjects(); - command.Execute(); - _undoCommands.Push(command); - UpdateSubjects(); + return command; + } - return command; - } - - private void UpdateSubjects() - { - _canUndo.OnNext(_undoCommands.Any()); - _canRedo.OnNext(_redoCommands.Any()); - } + private void UpdateSubjects() + { + _canUndo.OnNext(_undoCommands.Any()); + _canRedo.OnNext(_redoCommands.Any()); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 339cba6b1..d6f4828db 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using DynamicData; using Serilog; @@ -28,6 +29,7 @@ internal class ProfileEditorService : IProfileEditorService private readonly IProfileService _profileService; private readonly IModuleService _moduleService; private readonly IWindowService _windowService; + private ProfileEditorCommandScope? _profileEditorHistoryScope; public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService) { @@ -86,6 +88,9 @@ internal class ProfileEditorService : IProfileEditorService // Deselect whatever profile element was active ChangeCurrentProfileElement(null); + // Close the command scope if one was open + _profileEditorHistoryScope?.Dispose(); + // The new profile may need activation if (profileConfiguration != null) { @@ -205,11 +210,27 @@ internal class ProfileEditorService : IProfileEditorService return time; } + public TimeSpan RoundTime(TimeSpan time) + { + // Round the time to something that fits the current zoom level + if (_pixelsPerSecondSubject.Value < 50) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 200.0) * 200.0); + if (_pixelsPerSecondSubject.Value < 100) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 100.0) * 100.0); + if (_pixelsPerSecondSubject.Value < 200) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 50.0) * 50.0); + if (_pixelsPerSecondSubject.Value < 500) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 20.0) * 20.0); + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds)); + } + public void ChangePixelsPerSecond(int pixelsPerSecond) { _pixelsPerSecondSubject.OnNext(pixelsPerSecond); } + #region Commands + public void ExecuteCommand(IProfileEditorCommand command) { try @@ -218,6 +239,13 @@ internal class ProfileEditorService : IProfileEditorService if (history == null) throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration"); + // If a scope is active add the command to it, the scope will execute it immediately + if (_profileEditorHistoryScope != null) + { + _profileEditorHistoryScope.AddCommand(command); + return; + } + history.Execute.Execute(command).Subscribe(); } catch (Exception e) @@ -227,6 +255,36 @@ internal class ProfileEditorService : IProfileEditorService } } + public ProfileEditorCommandScope CreateCommandScope(string name) + { + if (_profileEditorHistoryScope != null) + throw new ArtemisSharedUIException($"A command scope is already active, name: {_profileEditorHistoryScope.Name}."); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + _profileEditorHistoryScope = new ProfileEditorCommandScope(this, name); + return _profileEditorHistoryScope; + } + + internal void StopCommandScope() + { + // This might happen if the scope is disposed twice, it's no biggie + if (_profileEditorHistoryScope == null) + return; + + ProfileEditorCommandScope scope = _profileEditorHistoryScope; + _profileEditorHistoryScope = null; + + // Executing the composite command won't do anything the first time (see last ctor variable) + // commands were already executed each time they were added to the scope + ExecuteCommand(new CompositeCommand(scope.ProfileEditorCommands, scope.Name, true)); + } + + #endregion + + + /// public void SaveProfile() { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs index 58953aed1..602eb1424 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs @@ -72,7 +72,7 @@ public class PropertiesView : ReactiveUserControl if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) { List snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList(); - newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ViewModel.PixelsPerSecond * 5), true, false, snapTimes); + newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, true, false, snapTimes); } // If holding down control, round to the closest 50ms 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 e90f1a699..c04b6857f 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 @@ -1,5 +1,6 @@ using System; using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; @@ -12,20 +13,20 @@ public interface ITimelineKeyframeViewModel #region Movement void Select(bool expand, bool toggle); - // void StartMovement(); - // void FinishMovement(); - - void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); - void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); - void UpdatePosition(TimeSpan position); - void ReleaseMovement(); + void StartMovement(ITimelineKeyframeViewModel source); + void UpdateMovement(TimeSpan position); + void FinishMovement(); + TimeSpan GetTimeSpanAtPosition(double x); #endregion #region Context menu actions void PopulateEasingViewModels(); - void Delete(bool save = true); + void Duplicate(); + void Copy(); + void Paste(); + void Delete(); #endregion } \ 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 a9f501b03..789084d06 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 @@ -17,7 +17,8 @@ Classes.selected="{Binding IsSelected}" PointerPressed="InputElement_OnPointerPressed" PointerReleased="InputElement_OnPointerReleased" - PointerMoved="InputElement_OnPointerMoved"> + PointerMoved="InputElement_OnPointerMoved" + Cursor="Hand">