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