1
0
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:
Robert 2022-01-20 00:24:48 +01:00
parent 98180df5f2
commit 6f269af8d4
19 changed files with 433 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,13 @@ 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
/// </summary>
public class SelectionRectangle : Control
{
/// <summary>
/// Defines the <see cref="Background" /> property.
/// </summary>
@ -35,7 +35,7 @@ namespace Artemis.UI.Shared.Controls
/// Defines the <see cref="BorderRadius" /> property.
/// </summary>
public static readonly StyledProperty<double> BorderRadiusProperty =
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderRadius), 0);
AvaloniaProperty.Register<SelectionRectangle, double>(nameof(BorderRadius));
/// <summary>
/// Defines the <see cref="InputElement" /> property.
@ -43,11 +43,18 @@ namespace Artemis.UI.Shared.Controls
public static readonly StyledProperty<IControl?> InputElementProperty =
AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged);
private Rect? _displayRect;
/// <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;
private Point _absoluteStartPosition;
/// <inheritdoc />
public SelectionRectangle()
@ -101,6 +108,15 @@ namespace Artemis.UI.Shared.Controls
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>
@ -165,6 +181,7 @@ namespace Artemis.UI.Shared.Controls
OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers));
InvalidateVisual();
IsSelecting = true;
}
private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e)
@ -182,6 +199,7 @@ namespace Artemis.UI.Shared.Controls
_displayRect = null;
InvalidateVisual();
IsSelecting = false;
}
private void SubscribeToInputElement()
@ -234,5 +252,4 @@ namespace Artemis.UI.Shared.Controls
}
#endregion
}
}

View File

@ -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.

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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);

View File

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

View File

@ -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));
}
}

View File

@ -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)

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

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