From 6f269af8d4582efb1ae54050d2d38ae04b769f8a Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Jan 2022 00:24:48 +0100 Subject: [PATCH] Profile editor service - Make keyframe selection an editor concern --- .../Profile/LayerProperties/ILayerProperty.cs | 7 + .../Profile/LayerProperties/LayerProperty.cs | 3 + .../Artemis.UI.Linux/packages.lock.json | 6 +- .../Artemis.UI.MacOS/packages.lock.json | 6 +- .../Artemis.UI.Shared.csproj | 1 + .../Controls/SelectionRectangle.cs | 465 +++++++++--------- .../ProfileEditor/IProfileEditorService.cs | 24 +- .../ProfileEditor/ProfileEditorService.cs | 58 +++ .../Artemis.UI.Shared/packages.lock.json | 17 +- .../Artemis.UI.Windows/packages.lock.json | 6 +- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 1 + .../Timeline/ITimelineKeyframeViewModel.cs | 6 +- .../Timeline/TimelineKeyframeView.axaml | 5 +- .../Timeline/TimelineKeyframeView.axaml.cs | 29 ++ .../Timeline/TimelineKeyframeViewModel.cs | 32 +- .../Properties/Timeline/TimelineView.axaml | 4 +- .../Properties/Timeline/TimelineView.axaml.cs | 32 +- .../Properties/Timeline/TimelineViewModel.cs | 65 +-- src/Avalonia/Artemis.UI/packages.lock.json | 18 +- 19 files changed, 433 insertions(+), 352 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 9a4fd8333..ddb54e1ee 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -52,6 +54,11 @@ namespace Artemis.Core /// string Path { get; } + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + ReadOnlyCollection UntypedKeyframes { get; } + /// /// Gets the type of the property /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 9cfb5769d..c2f46fad2 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -258,6 +258,9 @@ namespace Artemis.Core /// public ReadOnlyCollection> Keyframes { get; } + /// + public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); + /// /// Gets the current keyframe in the timeline according to the current progress /// diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 3bc042d07..684d112e1 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1752,6 +1752,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1774,6 +1775,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 3bc042d07..684d112e1 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1752,6 +1752,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1774,6 +1775,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 1cd6e870b..51947d759 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 3cd56734a..3866dcfb1 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -6,233 +6,250 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; -namespace Artemis.UI.Shared.Controls +namespace Artemis.UI.Shared.Controls; + +/// +/// Visualizes an with optional per-LED colors +/// +public class SelectionRectangle : Control { /// - /// Visualizes an with optional per-LED colors + /// Defines the property. /// - public class SelectionRectangle : Control + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderBrushProperty = + AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderThicknessProperty = + AvaloniaProperty.Register(nameof(BorderThickness), 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderRadiusProperty = + AvaloniaProperty.Register(nameof(BorderRadius)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty InputElementProperty = + AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); + + /// + /// Defines the read-only property. + /// + public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect(nameof(IsSelecting), o => o.IsSelecting); + + private Rect? _absoluteRect; + private Point _absoluteStartPosition; + + private Rect? _displayRect; + private bool _isSelecting; + private IControl? _oldInputElement; + private Point _startPosition; + + /// + public SelectionRectangle() { - /// - /// Defines the property. - /// - public static readonly StyledProperty BackgroundProperty = - AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderBrushProperty = - AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness), 1); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderRadiusProperty = - AvaloniaProperty.Register(nameof(BorderRadius), 0); - - /// - /// Defines the property. - /// - public static readonly StyledProperty InputElementProperty = - AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); - - private Rect? _displayRect; - private Rect? _absoluteRect; - private IControl? _oldInputElement; - private Point _startPosition; - private Point _absoluteStartPosition; - - /// - public SelectionRectangle() - { - AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); - IsHitTestVisible = false; - } - - /// - /// Gets or sets a brush used to paint the control's background. - /// - public IBrush Background - { - get => GetValue(BackgroundProperty); - set => SetValue(BackgroundProperty, value); - } - - /// - /// Gets or sets a brush used to paint the control's border - /// - public IBrush BorderBrush - { - get => GetValue(BorderBrushProperty); - set => SetValue(BorderBrushProperty, value); - } - - /// - /// Gets or sets the width of the control's border - /// - public double BorderThickness - { - get => GetValue(BorderThicknessProperty); - set => SetValue(BorderThicknessProperty, value); - } - - /// - /// Gets or sets the radius of the control's border - /// - public double BorderRadius - { - get => GetValue(BorderRadiusProperty); - set => SetValue(BorderRadiusProperty, value); - } - - /// - /// Gets or sets the element that captures input for the selection rectangle. - /// - public IControl? InputElement - { - get => GetValue(InputElementProperty); - set => SetValue(InputElementProperty, value); - } - - /// - /// Occurs when the selection rect is being updated, indicating the user is dragging. - /// - public event EventHandler? SelectionUpdated; - - /// - /// Occurs when the selection has finished, indicating the user stopped dragging. - /// - public event EventHandler? SelectionFinished; - - /// - /// Invokes the event - /// - /// - protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) - { - SelectionUpdated?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - /// - protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) - { - SelectionFinished?.Invoke(this, e); - } - - private static void OnInputElementChanged(IAvaloniaObject sender, bool before) - { - ((SelectionRectangle) sender).SubscribeToInputElement(); - } - - private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - e.Pointer.Capture(this); - - _startPosition = e.GetPosition(Parent); - _absoluteStartPosition = e.GetPosition(VisualRoot); - _displayRect = null; - } - - private void ParentOnPointerMoved(object? sender, PointerEventArgs e) - { - if (!ReferenceEquals(e.Pointer.Captured, this)) - return; - - Point currentPosition = e.GetPosition(Parent); - Point absoluteCurrentPosition = e.GetPosition(VisualRoot); - - _displayRect = new Rect( - new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), - new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) - ); - _absoluteRect = new Rect( - new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)), - new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)) - ); - - OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); - InvalidateVisual(); - } - - private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!ReferenceEquals(e.Pointer.Captured, this)) - return; - - e.Pointer.Capture(null); - - if (_displayRect != null && _absoluteRect != null) - { - OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); - e.Handled = true; - } - - _displayRect = null; - InvalidateVisual(); - } - - private void SubscribeToInputElement() - { - if (_oldInputElement != null) - { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; - _oldInputElement.PointerMoved -= ParentOnPointerMoved; - _oldInputElement.PointerReleased -= ParentOnPointerReleased; - } - - _oldInputElement = InputElement; - - if (InputElement != null) - { - InputElement.PointerPressed += ParentOnPointerPressed; - InputElement.PointerMoved += ParentOnPointerMoved; - InputElement.PointerReleased += ParentOnPointerReleased; - } - } - - #region Overrides of Visual - - /// - public override void Render(DrawingContext drawingContext) - { - if (_displayRect != null) - drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius); - } - - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - SubscribeToInputElement(); - base.OnAttachedToVisualTree(e); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - if (_oldInputElement != null) - { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; - _oldInputElement.PointerMoved -= ParentOnPointerMoved; - _oldInputElement.PointerReleased -= ParentOnPointerReleased; - _oldInputElement = null; - } - - base.OnDetachedFromVisualTree(e); - } - - #endregion + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); + IsHitTestVisible = false; } + + /// + /// Gets or sets a brush used to paint the control's background. + /// + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + /// + /// Gets or sets a brush used to paint the control's border + /// + public IBrush BorderBrush + { + get => GetValue(BorderBrushProperty); + set => SetValue(BorderBrushProperty, value); + } + + /// + /// Gets or sets the width of the control's border + /// + public double BorderThickness + { + get => GetValue(BorderThicknessProperty); + set => SetValue(BorderThicknessProperty, value); + } + + /// + /// Gets or sets the radius of the control's border + /// + public double BorderRadius + { + get => GetValue(BorderRadiusProperty); + set => SetValue(BorderRadiusProperty, value); + } + + /// + /// Gets or sets the element that captures input for the selection rectangle. + /// + public IControl? InputElement + { + get => GetValue(InputElementProperty); + set => SetValue(InputElementProperty, value); + } + + /// + /// Gets a boolean indicating whether the selection rectangle is currently performing a selection. + /// + public bool IsSelecting + { + get => _isSelecting; + private set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); + } + + /// + /// Occurs when the selection rect is being updated, indicating the user is dragging. + /// + public event EventHandler? SelectionUpdated; + + /// + /// Occurs when the selection has finished, indicating the user stopped dragging. + /// + public event EventHandler? SelectionFinished; + + /// + /// Invokes the event + /// + /// + protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) + { + SelectionUpdated?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) + { + SelectionFinished?.Invoke(this, e); + } + + private static void OnInputElementChanged(IAvaloniaObject sender, bool before) + { + ((SelectionRectangle) sender).SubscribeToInputElement(); + } + + private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Pointer.Capture(this); + + _startPosition = e.GetPosition(Parent); + _absoluteStartPosition = e.GetPosition(VisualRoot); + _displayRect = null; + } + + private void ParentOnPointerMoved(object? sender, PointerEventArgs e) + { + if (!ReferenceEquals(e.Pointer.Captured, this)) + return; + + Point currentPosition = e.GetPosition(Parent); + Point absoluteCurrentPosition = e.GetPosition(VisualRoot); + + _displayRect = new Rect( + new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), + new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) + ); + _absoluteRect = new Rect( + new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)), + new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)) + ); + + OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); + InvalidateVisual(); + IsSelecting = true; + } + + private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!ReferenceEquals(e.Pointer.Captured, this)) + return; + + e.Pointer.Capture(null); + + if (_displayRect != null && _absoluteRect != null) + { + OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); + e.Handled = true; + } + + _displayRect = null; + InvalidateVisual(); + IsSelecting = false; + } + + private void SubscribeToInputElement() + { + if (_oldInputElement != null) + { + _oldInputElement.PointerPressed -= ParentOnPointerPressed; + _oldInputElement.PointerMoved -= ParentOnPointerMoved; + _oldInputElement.PointerReleased -= ParentOnPointerReleased; + } + + _oldInputElement = InputElement; + + if (InputElement != null) + { + InputElement.PointerPressed += ParentOnPointerPressed; + InputElement.PointerMoved += ParentOnPointerMoved; + InputElement.PointerReleased += ParentOnPointerReleased; + } + } + + #region Overrides of Visual + + /// + public override void Render(DrawingContext drawingContext) + { + if (_displayRect != null) + drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + SubscribeToInputElement(); + base.OnAttachedToVisualTree(e); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (_oldInputElement != null) + { + _oldInputElement.PointerPressed -= ParentOnPointerPressed; + _oldInputElement.PointerMoved -= ParentOnPointerMoved; + _oldInputElement.PointerReleased -= ParentOnPointerReleased; + _oldInputElement = null; + } + + base.OnDetachedFromVisualTree(e); + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 40cb889a5..9ce0edf0b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services.Interfaces; +using DynamicData; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -40,7 +41,13 @@ public interface IProfileEditorService : IArtemisSharedUIService /// Gets an observable of the zoom level. /// IObservable PixelsPerSecond { get; } - + + /// + /// 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(); + /// /// Changes the selected profile by its . /// @@ -65,6 +72,21 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The new pixels per second. void ChangePixelsPerSecond(int pixelsPerSecond); + /// + /// 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 . + void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle); + + /// + /// Selects the provided keyframes. + /// + /// The keyframes to select. + /// If expands the current selection; otherwise replaces it with only the provided . + void SelectKeyframes(IEnumerable keyframes, bool expand); + /// /// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a /// segment end. diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 54de02885..339cba6b1 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -7,6 +8,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using DynamicData; using Serilog; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -20,6 +22,8 @@ internal class ProfileEditorService : IProfileEditorService private readonly BehaviorSubject _playingSubject = new(false); private readonly BehaviorSubject _suspendedEditingSubject = new(false); private readonly BehaviorSubject _pixelsPerSecondSubject = new(120); + private readonly SourceList _selectedKeyframes = new(); + private readonly ILogger _logger; private readonly IProfileService _profileService; private readonly IModuleService _moduleService; @@ -60,6 +64,7 @@ internal class ProfileEditorService : IProfileEditorService public IObservable Playing { get; } public IObservable SuspendedEditing { get; } public IObservable PixelsPerSecond { get; } + public IObservable> ConnectToKeyframes() => _selectedKeyframes.Connect(); public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { @@ -97,11 +102,13 @@ internal class ProfileEditorService : IProfileEditorService _moduleService.SetActivationOverride(null); _profileService.RenderForEditor = false; } + _profileConfigurationSubject.OnNext(profileConfiguration); } public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) { + _selectedKeyframes.Clear(); _profileElementSubject.OnNext(renderProfileElement); } @@ -111,6 +118,57 @@ internal class ProfileEditorService : IProfileEditorService _timeSubject.OnNext(time); } + public void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle) + { + if (keyframe == null) + { + if (!expand) + _selectedKeyframes.Clear(); + return; + } + + if (toggle) + { + // Toggle only the clicked keyframe, leave others alone + if (_selectedKeyframes.Items.Contains(keyframe)) + _selectedKeyframes.Remove(keyframe); + else + _selectedKeyframes.Add(keyframe); + } + else + { + if (expand) + { + _selectedKeyframes.Add(keyframe); + } + else + { + _selectedKeyframes.Edit(l => + { + l.Clear(); + l.Add(keyframe); + }); + } + } + } + + public void SelectKeyframes(IEnumerable keyframes, bool expand) + { + if (expand) + { + List toAdd = keyframes.Except(_selectedKeyframes.Items).ToList(); + _selectedKeyframes.AddRange(toAdd); + } + else + { + _selectedKeyframes.Edit(l => + { + l.Clear(); + l.AddRange(keyframes); + }); + } + } + public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List? snapTimes = null) { RenderProfileElement? profileElement = _profileElementSubject.Value; diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index ee416f1bd..7bd70aa2e 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -70,6 +70,15 @@ "Avalonia": "0.10.11" } }, + "DynamicData": { + "type": "Direct", + "requested": "[7.4.9, )", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, "FluentAvaloniaUI": { "type": "Direct", "requested": "[1.1.8, )", @@ -240,14 +249,6 @@ "System.Xml.XmlDocument": "4.3.0" } }, - "DynamicData": { - "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", - "dependencies": { - "System.Reactive": "5.0.0" - } - }, "EmbedIO": { "type": "Transitive", "resolved": "3.4.3", diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index 213018047..b5bc4d22b 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -217,8 +217,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1768,6 +1768,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1790,6 +1791,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index a3a0e83e1..21e8cea0e 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -21,6 +21,7 @@ + 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 4dfaa86d2..d91a2d9a5 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 @@ -5,12 +5,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; public interface ITimelineKeyframeViewModel { - bool IsSelected { get; set; } + bool IsSelected { get; } TimeSpan Position { get; } ILayerPropertyKeyframe Keyframe { get; } #region Movement + void Select(bool expand, bool toggle); + // void StartMovement(); + // void FinishMovement(); + void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); void UpdatePosition(TimeSpan position); 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 fb1782854..6bcd67db5 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 @@ -13,7 +13,10 @@ Height="10" Margin="-5,0,0,0" ToolTip.Tip="{Binding Timestamp}" - Classes.selected="{Binding IsSelected}"> + Classes.selected="{Binding IsSelected}" + PointerPressed="InputElement_OnPointerPressed" + PointerReleased="InputElement_OnPointerReleased" + PointerMoved="InputElement_OnPointerMoved">