mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor service - Make keyframe selection an editor concern
This commit is contained in:
parent
98180df5f2
commit
6f269af8d4
@ -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
|
||||
/// </summary>
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only list of all the keyframes on this layer property
|
||||
/// </summary>
|
||||
ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the property
|
||||
/// </summary>
|
||||
|
||||
@ -258,6 +258,9 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<LayerPropertyKeyframe<T>> Keyframes { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes => new(Keyframes.Cast<ILayerPropertyKeyframe>().ToList());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current keyframe in the timeline according to the current progress
|
||||
/// </summary>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.11.5" />
|
||||
<PackageReference Include="Avalonia.Xaml.Interactions" Version="0.10.11.5" />
|
||||
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="0.10.11.5" />
|
||||
<PackageReference Include="DynamicData" Version="7.4.9" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="1.1.8" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
|
||||
<PackageReference Include="ReactiveUI" Version="16.3.10" />
|
||||
|
||||
@ -6,233 +6,250 @@ using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Artemis.UI.Shared.Controls
|
||||
namespace Artemis.UI.Shared.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
|
||||
/// </summary>
|
||||
public class SelectionRectangle : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
|
||||
/// Defines the <see cref="Background" /> property.
|
||||
/// </summary>
|
||||
public class SelectionRectangle : Control
|
||||
public static readonly StyledProperty<IBrush> BackgroundProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderBrush" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IBrush> BorderBrushProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderThickness" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> BorderThicknessProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderThickness), 1);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderRadius" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> BorderRadiusProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderRadius));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="InputElement" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IControl?> InputElementProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the read-only <see cref="IsSelecting" /> property.
|
||||
/// </summary>
|
||||
public static readonly DirectProperty<SelectionRectangle, bool> IsSelectingProperty = AvaloniaProperty.RegisterDirect<SelectionRectangle, bool>(nameof(IsSelecting), o => o.IsSelecting);
|
||||
|
||||
private Rect? _absoluteRect;
|
||||
private Point _absoluteStartPosition;
|
||||
|
||||
private Rect? _displayRect;
|
||||
private bool _isSelecting;
|
||||
private IControl? _oldInputElement;
|
||||
private Point _startPosition;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SelectionRectangle()
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Background" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IBrush> BackgroundProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderBrush" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IBrush> BorderBrushProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IBrush>(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderThickness" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> BorderThicknessProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderThickness), 1);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="BorderRadius" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> BorderRadiusProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderRadius), 0);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="InputElement" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IControl?> InputElementProperty =
|
||||
AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged);
|
||||
|
||||
private Rect? _displayRect;
|
||||
private Rect? _absoluteRect;
|
||||
private IControl? _oldInputElement;
|
||||
private Point _startPosition;
|
||||
private Point _absoluteStartPosition;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SelectionRectangle()
|
||||
{
|
||||
AffectsRender<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty);
|
||||
IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brush used to paint the control's background.
|
||||
/// </summary>
|
||||
public IBrush Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brush used to paint the control's border
|
||||
/// </summary>
|
||||
public IBrush BorderBrush
|
||||
{
|
||||
get => GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the control's border
|
||||
/// </summary>
|
||||
public double BorderThickness
|
||||
{
|
||||
get => GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the radius of the control's border
|
||||
/// </summary>
|
||||
public double BorderRadius
|
||||
{
|
||||
get => GetValue(BorderRadiusProperty);
|
||||
set => SetValue(BorderRadiusProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element that captures input for the selection rectangle.
|
||||
/// </summary>
|
||||
public IControl? InputElement
|
||||
{
|
||||
get => GetValue(InputElementProperty);
|
||||
set => SetValue(InputElementProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the selection rect is being updated, indicating the user is dragging.
|
||||
/// </summary>
|
||||
public event EventHandler<SelectionRectangleEventArgs>? SelectionUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the selection has finished, indicating the user stopped dragging.
|
||||
/// </summary>
|
||||
public event EventHandler<SelectionRectangleEventArgs>? SelectionFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="SelectionUpdated" /> event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e)
|
||||
{
|
||||
SelectionUpdated?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="SelectionFinished" /> event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Render(DrawingContext drawingContext)
|
||||
{
|
||||
if (_displayRect != null)
|
||||
drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
SubscribeToInputElement();
|
||||
base.OnAttachedToVisualTree(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<TextBlock>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty);
|
||||
IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brush used to paint the control's background.
|
||||
/// </summary>
|
||||
public IBrush Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brush used to paint the control's border
|
||||
/// </summary>
|
||||
public IBrush BorderBrush
|
||||
{
|
||||
get => GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the control's border
|
||||
/// </summary>
|
||||
public double BorderThickness
|
||||
{
|
||||
get => GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the radius of the control's border
|
||||
/// </summary>
|
||||
public double BorderRadius
|
||||
{
|
||||
get => GetValue(BorderRadiusProperty);
|
||||
set => SetValue(BorderRadiusProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element that captures input for the selection rectangle.
|
||||
/// </summary>
|
||||
public IControl? InputElement
|
||||
{
|
||||
get => GetValue(InputElementProperty);
|
||||
set => SetValue(InputElementProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the selection rectangle is currently performing a selection.
|
||||
/// </summary>
|
||||
public bool IsSelecting
|
||||
{
|
||||
get => _isSelecting;
|
||||
private set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the selection rect is being updated, indicating the user is dragging.
|
||||
/// </summary>
|
||||
public event EventHandler<SelectionRectangleEventArgs>? SelectionUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the selection has finished, indicating the user stopped dragging.
|
||||
/// </summary>
|
||||
public event EventHandler<SelectionRectangleEventArgs>? SelectionFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="SelectionUpdated" /> event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e)
|
||||
{
|
||||
SelectionUpdated?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="SelectionFinished" /> event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Render(DrawingContext drawingContext)
|
||||
{
|
||||
if (_displayRect != null)
|
||||
drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
SubscribeToInputElement();
|
||||
base.OnAttachedToVisualTree(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (_oldInputElement != null)
|
||||
{
|
||||
_oldInputElement.PointerPressed -= ParentOnPointerPressed;
|
||||
_oldInputElement.PointerMoved -= ParentOnPointerMoved;
|
||||
_oldInputElement.PointerReleased -= ParentOnPointerReleased;
|
||||
_oldInputElement = null;
|
||||
}
|
||||
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -41,6 +42,12 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
/// </summary>
|
||||
IObservable<int> PixelsPerSecond { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the observable list of keyframes and observe any changes starting with the list's initial items.
|
||||
/// </summary>
|
||||
/// <returns>An observable which emits the change set.</returns>
|
||||
IObservable<IChangeSet<ILayerPropertyKeyframe>> ConnectToKeyframes();
|
||||
|
||||
/// <summary>
|
||||
/// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />.
|
||||
/// </summary>
|
||||
@ -65,6 +72,21 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
/// <param name="pixelsPerSecond">The new pixels per second.</param>
|
||||
void ChangePixelsPerSecond(int pixelsPerSecond);
|
||||
|
||||
/// <summary>
|
||||
/// Selects the provided keyframe.
|
||||
/// </summary>
|
||||
/// <param name="keyframe">The keyframe to select.</param>
|
||||
/// <param name="expand">If <see langword="true"/> expands the current selection; otherwise replaces it with only the provided <paramref name="keyframe"/>.</param>
|
||||
/// <param name="toggle">If <see langword="true"/> toggles the selection and only for the provided <paramref name="keyframe"/>.</param>
|
||||
void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle);
|
||||
|
||||
/// <summary>
|
||||
/// Selects the provided keyframes.
|
||||
/// </summary>
|
||||
/// <param name="keyframes">The keyframes to select.</param>
|
||||
/// <param name="expand">If <see langword="true"/> expands the current selection; otherwise replaces it with only the provided <paramref name="keyframes"/>.</param>
|
||||
void SelectKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, bool expand);
|
||||
|
||||
/// <summary>
|
||||
/// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a
|
||||
/// segment end.
|
||||
|
||||
@ -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<bool> _playingSubject = new(false);
|
||||
private readonly BehaviorSubject<bool> _suspendedEditingSubject = new(false);
|
||||
private readonly BehaviorSubject<int> _pixelsPerSecondSubject = new(120);
|
||||
private readonly SourceList<ILayerPropertyKeyframe> _selectedKeyframes = new();
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IModuleService _moduleService;
|
||||
@ -60,6 +64,7 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
public IObservable<bool> Playing { get; }
|
||||
public IObservable<bool> SuspendedEditing { get; }
|
||||
public IObservable<int> PixelsPerSecond { get; }
|
||||
public IObservable<IChangeSet<ILayerPropertyKeyframe>> 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<ILayerPropertyKeyframe> keyframes, bool expand)
|
||||
{
|
||||
if (expand)
|
||||
{
|
||||
List<ILayerPropertyKeyframe> 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<TimeSpan>? snapTimes = null)
|
||||
{
|
||||
RenderProfileElement? profileElement = _profileElementSubject.Value;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.11" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.11" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.11" />
|
||||
<PackageReference Include="DynamicData" Version="7.4.9" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="1.1.8" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
||||
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
<Ellipse.Styles>
|
||||
<Style Selector="Ellipse">
|
||||
<Setter Property="StrokeThickness" Value="0" />
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
@ -5,6 +6,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
|
||||
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
||||
{
|
||||
private bool _moved;
|
||||
|
||||
public TimelineKeyframeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@ -14,4 +17,30 @@ public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewMod
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
e.Pointer.Capture((IInputElement?) sender);
|
||||
e.Handled = true;
|
||||
|
||||
_moved = false;
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (e.Pointer.Captured != sender)
|
||||
return;
|
||||
|
||||
_moved = true;
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
e.Pointer.Capture(null);
|
||||
e.Handled = true;
|
||||
|
||||
// Select the keyframe if the user didn't move
|
||||
if (!_moved)
|
||||
ViewModel?.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
using Disposable = System.Reactive.Disposables.Disposable;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
|
||||
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
||||
{
|
||||
private bool _isSelected;
|
||||
private string _timestamp;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
private double _x;
|
||||
private string _timestamp;
|
||||
private ObservableAsPropertyHelper<bool>? _isSelected;
|
||||
|
||||
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
|
||||
{
|
||||
IProfileEditorService profileEditorService1 = profileEditorService;
|
||||
_profileEditorService = profileEditorService;
|
||||
_timestamp = "0.000";
|
||||
LayerPropertyKeyframe = layerPropertyKeyframe;
|
||||
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService1.PixelsPerSecond.Subscribe(p =>
|
||||
profileEditorService.PixelsPerSecond.Subscribe(p =>
|
||||
{
|
||||
_pixelsPerSecond = p;
|
||||
profileEditorService1.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
|
||||
Disposable.Create(() =>
|
||||
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);
|
||||
profileEditorService.ConnectToKeyframes();
|
||||
});
|
||||
}
|
||||
|
||||
@ -54,21 +59,22 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
||||
set => this.RaiseAndSetIfChanged(ref _timestamp, value);
|
||||
}
|
||||
|
||||
public bool IsSelected => _isSelected?.Value ?? false;
|
||||
public TimeSpan Position => LayerPropertyKeyframe.Position;
|
||||
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
|
||||
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
/// <inheritdoc />
|
||||
public void Select(bool expand, bool toggle)
|
||||
{
|
||||
get => _isSelected;
|
||||
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
|
||||
_profileEditorService.SelectKeyframe(Keyframe, expand, toggle);
|
||||
}
|
||||
|
||||
public TimeSpan Position => LayerPropertyKeyframe.Position;
|
||||
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public void Delete(bool save = true)
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<x:Double x:Key="RailsHeight">28</x:Double>
|
||||
<x:Double x:Key="RailsBorderHeight">29</x:Double>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="Transparent" PointerMoved="InputElement_OnPointerMoved" PointerReleased="InputElement_OnPointerReleased">
|
||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased">
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||
@ -18,7 +18,7 @@
|
||||
</TreeDataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<controls:SelectionRectangle InputElement="{Binding $parent}" SelectionFinished="SelectionRectangle_OnSelectionFinished"></controls:SelectionRectangle>
|
||||
<controls:SelectionRectangle Name="SelectionRectangle" InputElement="{Binding $parent}" SelectionFinished="SelectionRectangle_OnSelectionFinished"></controls:SelectionRectangle>
|
||||
</Grid>
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Shared.Controls;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Artemis.UI.Shared.Extensions;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
@ -11,11 +13,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
|
||||
public class TimelineView : ReactiveUserControl<TimelineViewModel>
|
||||
{
|
||||
private bool _draggedCursor;
|
||||
private readonly SelectionRectangle _selectionRectangle;
|
||||
|
||||
public TimelineView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_selectionRectangle = this.Get<SelectionRectangle>("SelectionRectangle");
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
@ -34,35 +37,14 @@ public class TimelineView : ReactiveUserControl<TimelineViewModel>
|
||||
return e.AbsoluteRectangle.Intersects(hitTestRect);
|
||||
}).ToList();
|
||||
|
||||
ViewModel.SelectKeyframes(keyframeViews.Where(kv => kv.ViewModel != null).Select(kv => kv.ViewModel!).ToList(), e.KeyModifiers.HasFlag(KeyModifiers.Shift));
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (_draggedCursor)
|
||||
return;
|
||||
|
||||
_draggedCursor = e.GetCurrentPoint(this).Properties.IsLeftButtonPressed;
|
||||
ViewModel.SelectKeyframes(keyframeViews.Where(kv => kv.ViewModel != null).Select(kv => kv.ViewModel!), e.KeyModifiers.HasFlag(KeyModifiers.Shift));
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
if (_selectionRectangle.IsSelecting)
|
||||
return;
|
||||
|
||||
if (_draggedCursor)
|
||||
{
|
||||
_draggedCursor = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Point position = e.GetPosition(VisualRoot);
|
||||
TimelineKeyframeView? keyframeView = this.GetVisualChildrenOfType<TimelineKeyframeView>().Where(k =>
|
||||
{
|
||||
Rect hitTestRect = k.TransformedBounds != null ? k.TransformedBounds.Value.Bounds.TransformToAABB(k.TransformedBounds.Value.Transform) : Rect.Empty;
|
||||
return hitTestRect.Contains(position);
|
||||
}).FirstOrDefault(kv => kv.ViewModel != null);
|
||||
|
||||
ViewModel.SelectKeyframe(keyframeView?.ViewModel, e.KeyModifiers.HasFlag(KeyModifiers.Shift), false);
|
||||
ViewModel?.SelectKeyframes(new List<ITimelineKeyframeViewModel>(), false);
|
||||
}
|
||||
}
|
||||
@ -43,69 +43,8 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
|
||||
}
|
||||
|
||||
public void SelectKeyframes(List<ITimelineKeyframeViewModel> keyframes, bool expand)
|
||||
public void SelectKeyframes(IEnumerable<ITimelineKeyframeViewModel> keyframes, bool expand)
|
||||
{
|
||||
List<ITimelineKeyframeViewModel> expandedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).ToList();
|
||||
List<ITimelineKeyframeViewModel> collapsedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(false)).Except(expandedKeyframes).ToList();
|
||||
|
||||
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in collapsedKeyframes)
|
||||
timelineKeyframeViewModel.IsSelected = false;
|
||||
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in expandedKeyframes)
|
||||
{
|
||||
if (timelineKeyframeViewModel.IsSelected && expand)
|
||||
continue;
|
||||
timelineKeyframeViewModel.IsSelected = keyframes.Contains(timelineKeyframeViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectKeyframe(ITimelineKeyframeViewModel? clicked, bool selectBetween, bool toggle)
|
||||
{
|
||||
List<ITimelineKeyframeViewModel> expandedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).ToList();
|
||||
List<ITimelineKeyframeViewModel> collapsedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(false)).Except(expandedKeyframes).ToList();
|
||||
|
||||
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in collapsedKeyframes)
|
||||
timelineKeyframeViewModel.IsSelected = false;
|
||||
|
||||
if (clicked == null)
|
||||
{
|
||||
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in expandedKeyframes)
|
||||
timelineKeyframeViewModel.IsSelected = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectBetween)
|
||||
{
|
||||
int selectedIndex = expandedKeyframes.FindIndex(k => k.IsSelected);
|
||||
// If nothing is selected, select only the clicked
|
||||
if (selectedIndex == -1)
|
||||
{
|
||||
clicked.IsSelected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes)
|
||||
keyframeViewModel.IsSelected = false;
|
||||
|
||||
int clickedIndex = expandedKeyframes.IndexOf(clicked);
|
||||
if (clickedIndex < selectedIndex)
|
||||
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
|
||||
keyframeViewModel.IsSelected = true;
|
||||
else
|
||||
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
|
||||
keyframeViewModel.IsSelected = true;
|
||||
}
|
||||
else if (toggle)
|
||||
{
|
||||
// Toggle only the clicked keyframe, leave others alone
|
||||
clicked.IsSelected = !clicked.IsSelected;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only select the clicked keyframe
|
||||
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes)
|
||||
keyframeViewModel.IsSelected = false;
|
||||
clicked.IsSelected = true;
|
||||
}
|
||||
_profileEditorService.SelectKeyframes(keyframes.Select(k => k.Keyframe), expand);
|
||||
}
|
||||
}
|
||||
@ -74,6 +74,15 @@
|
||||
"Svg.Skia": "0.5.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, )",
|
||||
@ -287,14 +296,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",
|
||||
@ -1762,6 +1763,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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user