mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor - Ported time caret
This commit is contained in:
parent
1832a25426
commit
098c44ebc8
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.UI.Shared.Services.Interfaces;
|
using Artemis.UI.Shared.Services.Interfaces;
|
||||||
@ -59,6 +60,17 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
|||||||
/// <param name="time">The new time.</param>
|
/// <param name="time">The new time.</param>
|
||||||
void ChangeTime(TimeSpan time);
|
void ChangeTime(TimeSpan time);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a segment end.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time to snap.</param>
|
||||||
|
/// <param name="tolerance">How close the time must be to snap.</param>
|
||||||
|
/// <param name="snapToSegments">Enable snapping to timeline segments.</param>
|
||||||
|
/// <param name="snapToCurrentTime">Enable snapping to the current time of the editor.</param>
|
||||||
|
/// <param name="snapTimes">An optional extra list of times to snap to.</param>
|
||||||
|
/// <returns>The snapped time.</returns>
|
||||||
|
TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the provided command and adds it to the history.
|
/// Executes the provided command and adds it to the history.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -110,6 +111,42 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
_timeSubject.OnNext(time);
|
_timeSubject.OnNext(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
|
||||||
|
{
|
||||||
|
RenderProfileElement? profileElement = _profileElementSubject.Value;
|
||||||
|
if (snapToSegments && profileElement != null)
|
||||||
|
{
|
||||||
|
// Snap to the end of the start segment
|
||||||
|
if (Math.Abs(time.TotalMilliseconds - profileElement.Timeline.StartSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
|
||||||
|
return profileElement.Timeline.StartSegmentEndPosition;
|
||||||
|
|
||||||
|
// Snap to the end of the main segment
|
||||||
|
if (Math.Abs(time.TotalMilliseconds - profileElement.Timeline.MainSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
|
||||||
|
return profileElement.Timeline.MainSegmentEndPosition;
|
||||||
|
|
||||||
|
// Snap to the end of the end segment (end of the timeline)
|
||||||
|
if (Math.Abs(time.TotalMilliseconds - profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
|
||||||
|
return profileElement.Timeline.EndSegmentEndPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap to the current time
|
||||||
|
if (snapToCurrentTime)
|
||||||
|
{
|
||||||
|
if (Math.Abs(time.TotalMilliseconds - _timeSubject.Value.TotalMilliseconds) < tolerance.TotalMilliseconds)
|
||||||
|
return _timeSubject.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapTimes != null)
|
||||||
|
{
|
||||||
|
// Find the closest keyframe
|
||||||
|
TimeSpan closeSnapTime = snapTimes.FirstOrDefault(s => Math.Abs(time.TotalMilliseconds - s.TotalMilliseconds) < tolerance.TotalMilliseconds)!;
|
||||||
|
if (closeSnapTime != TimeSpan.Zero)
|
||||||
|
return closeSnapTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
public void ChangePixelsPerSecond(double pixelsPerSecond)
|
public void ChangePixelsPerSecond(double pixelsPerSecond)
|
||||||
{
|
{
|
||||||
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
||||||
|
|||||||
@ -6,42 +6,103 @@
|
|||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView">
|
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView">
|
||||||
|
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
|
||||||
|
<Grid RowDefinitions="48,*">
|
||||||
|
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}"></ContentControl>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto,*" RowDefinitions="48,*">
|
<ScrollViewer Grid.Row="1"
|
||||||
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}"></ContentControl>
|
Grid.Column="0"
|
||||||
|
Name="TreeScrollViewer"
|
||||||
|
Offset="{Binding #TimelineScrollViewer.Offset, Mode=OneWay}"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
||||||
|
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||||
|
<ContentControl Content="{Binding TreeGroupViewModel}" />
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<GridSplitter Grid.Row="0" Grid.Column="1" Cursor="SizeWestEast" Foreground="Transparent" Background="Transparent" />
|
<GridSplitter Grid.Column="1"
|
||||||
|
Cursor="SizeWestEast"
|
||||||
|
Foreground="Transparent"
|
||||||
|
Background="Transparent"
|
||||||
|
Margin="0 0 -5 0"/>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1"
|
<Grid Grid.Column="2" RowDefinitions="48,*">
|
||||||
Grid.Column="0"
|
|
||||||
Name="TreeScrollViewer"
|
<Canvas Grid.Row="0" ZIndex="2">
|
||||||
Offset="{Binding #TimelineScrollViewer.Offset, Mode=OneWay}"
|
<!-- Timeline segments -->
|
||||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" />
|
||||||
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" />
|
||||||
<ItemsControl.ItemTemplate>
|
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" />
|
||||||
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
|
||||||
<ContentControl Content="{Binding TreeGroupViewModel}" />
|
<!-- Timeline header body -->
|
||||||
</TreeDataTemplate>
|
<!-- <controls:PropertyTimelineHeader Margin="0 18 0 0" -->
|
||||||
</ItemsControl.ItemTemplate>
|
<!-- Foreground="{DynamicResource MaterialDesignBody}" -->
|
||||||
</ItemsControl>
|
<!-- Background="{DynamicResource MaterialDesignCardBackground}" -->
|
||||||
</ScrollViewer>
|
<!-- PixelsPerSecond="{Binding ProfileEditorService.PixelsPerSecond}" -->
|
||||||
|
<!-- HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}" -->
|
||||||
|
<!-- VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}" -->
|
||||||
|
<!-- OffsetFirstValue="True" -->
|
||||||
|
<!-- MouseLeftButtonUp="{s:Action TimelineJump}" -->
|
||||||
|
<!-- Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" -->
|
||||||
|
<!-- Cursor="Hand" /> -->
|
||||||
|
|
||||||
|
<!-- Timeline caret -->
|
||||||
|
<Polygon Name="TimelineCaret"
|
||||||
|
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||||
|
Cursor="SizeWestEast"
|
||||||
|
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||||
|
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||||
|
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||||
|
Points="-8,0 -8,8 0,20, 8,8 8,0"
|
||||||
|
Fill="{DynamicResource SystemAccentColorLight1}">
|
||||||
|
<Polygon.Transitions>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition>
|
||||||
|
</Transitions>
|
||||||
|
</Polygon.Transitions>
|
||||||
|
</Polygon>
|
||||||
|
<Line Name="TimelineLine"
|
||||||
|
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||||
|
Cursor="SizeWestEast"
|
||||||
|
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||||
|
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||||
|
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||||
|
StartPoint="0,0"
|
||||||
|
EndPoint="0,1"
|
||||||
|
StrokeThickness="2"
|
||||||
|
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||||
|
RenderTransformOrigin="0,0">
|
||||||
|
<Line.Transitions>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition>
|
||||||
|
</Transitions>
|
||||||
|
</Line.Transitions>
|
||||||
|
<Line.RenderTransform>
|
||||||
|
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}"></ScaleTransform>
|
||||||
|
</Line.RenderTransform>
|
||||||
|
</Line>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
<!-- Horizontal scrolling -->
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<ScrollViewer Name="TimelineScrollViewer"
|
||||||
|
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
||||||
|
VerticalScrollBarVisibility="Hidden"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
||||||
|
<ContentControl Content="{Binding TimelineViewModel}"></ContentControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- TODO: Databindings here -->
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<GridSplitter Grid.Row="1" Grid.Column="1" Cursor="SizeWestEast" Foreground="Transparent" Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1"
|
|
||||||
Grid.Column="2"
|
|
||||||
Name="TimelineScrollViewer"
|
|
||||||
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
|
||||||
VerticalScrollBarVisibility="Hidden"
|
|
||||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
|
||||||
<ItemsControl Items="{Binding PropertyGroupViewModels}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
|
||||||
<ContentControl Content="{Binding TreeGroupViewModel}" />
|
|
||||||
</TreeDataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,10 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Animation;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||||
|
|
||||||
public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPropertiesViewModel>
|
public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPropertiesViewModel>
|
||||||
{
|
{
|
||||||
|
private Polygon _timelineCaret;
|
||||||
|
private Line _timelineLine;
|
||||||
|
|
||||||
public ProfileElementPropertiesView()
|
public ProfileElementPropertiesView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -13,5 +24,69 @@ public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPr
|
|||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
_timelineCaret = this.Get<Polygon>("TimelineCaret");
|
||||||
|
_timelineLine = this.Get<Line>("TimelineLine");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyTransition(bool enable)
|
||||||
|
{
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
|
||||||
|
((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.Zero;
|
||||||
|
((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TimelineCaret_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
e.Pointer.Capture((IInputElement?) sender);
|
||||||
|
ApplyTransition(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TimelineCaret_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
ApplyTransition(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TimelineCaret_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IInputElement? senderElement = (IInputElement?) sender;
|
||||||
|
if (senderElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get the parent grid, need that for our position
|
||||||
|
IVisual? parent = senderElement.VisualParent;
|
||||||
|
double x = Math.Max(0, e.GetPosition(parent).X);
|
||||||
|
TimeSpan newTime = TimeSpan.FromSeconds(x / ViewModel.PixelsPerSecond);
|
||||||
|
|
||||||
|
// Round the time to something that fits the current zoom level
|
||||||
|
if (ViewModel.PixelsPerSecond < 200)
|
||||||
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
|
||||||
|
else if (ViewModel.PixelsPerSecond < 500)
|
||||||
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
|
||||||
|
else
|
||||||
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
|
||||||
|
|
||||||
|
// If holding down shift, snap to the closest segment or keyframe
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
|
{
|
||||||
|
List<TimeSpan> snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
|
||||||
|
newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ViewModel.PixelsPerSecond * 5), true, false, snapTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If holding down control, round to the closest 50ms
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
|
||||||
|
|
||||||
|
ViewModel.TimelineViewModel.ChangeTime(newTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,6 +10,7 @@ using Artemis.Core.LayerBrushes;
|
|||||||
using Artemis.Core.LayerEffects;
|
using Artemis.Core.LayerEffects;
|
||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Playback;
|
using Artemis.UI.Screens.ProfileEditor.Playback;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@ -19,17 +20,21 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
|||||||
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel> _cachedViewModels;
|
private readonly Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel> _cachedViewModels;
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||||
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
|
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
|
||||||
|
private ObservableAsPropertyHelper<double>? _pixelsPerSecond;
|
||||||
private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels;
|
private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
|
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel, TimelineViewModel timelineViewModel)
|
||||||
{
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||||
_propertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>();
|
_propertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>();
|
||||||
_cachedViewModels = new Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel>();
|
_cachedViewModels = new Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel>();
|
||||||
PlaybackViewModel = playbackViewModel;
|
PlaybackViewModel = playbackViewModel;
|
||||||
|
TimelineViewModel = timelineViewModel;
|
||||||
|
|
||||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||||
@ -46,20 +51,27 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
|||||||
.Subscribe(_ => UpdateGroups());
|
.Subscribe(_ => UpdateGroups());
|
||||||
// React to service profile element changes as long as the VM is active
|
// React to service profile element changes as long as the VM is active
|
||||||
|
|
||||||
this.WhenActivated(d => _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d));
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
|
||||||
|
_pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
|
||||||
|
});
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
|
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaybackViewModel PlaybackViewModel { get; }
|
public PlaybackViewModel PlaybackViewModel { get; }
|
||||||
|
public TimelineViewModel TimelineViewModel { get; }
|
||||||
public RenderProfileElement? ProfileElement => _profileElement?.Value;
|
public RenderProfileElement? ProfileElement => _profileElement?.Value;
|
||||||
public Layer? Layer => _profileElement?.Value as Layer;
|
public Layer? Layer => _profileElement?.Value as Layer;
|
||||||
|
public double PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||||
|
public IObservable<bool> Playing => _profileEditorService.Playing;
|
||||||
|
|
||||||
public ObservableCollection<ProfileElementPropertyGroupViewModel> PropertyGroupViewModels
|
public ObservableCollection<ProfileElementPropertyGroupViewModel> PropertyGroupViewModels
|
||||||
{
|
{
|
||||||
get => _propertyGroupViewModels;
|
get => _propertyGroupViewModels;
|
||||||
set => this.RaiseAndSetIfChanged(ref _propertyGroupViewModels, value);
|
set => this.RaiseAndSetIfChanged(ref _propertyGroupViewModels, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateGroups()
|
private void UpdateGroups()
|
||||||
{
|
{
|
||||||
if (ProfileElement == null)
|
if (ProfileElement == null)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -6,6 +7,7 @@ using Artemis.Core;
|
|||||||
using Artemis.Core.LayerBrushes;
|
using Artemis.Core.LayerBrushes;
|
||||||
using Artemis.Core.LayerEffects;
|
using Artemis.Core.LayerEffects;
|
||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.PropertyInput;
|
using Artemis.UI.Shared.Services.PropertyInput;
|
||||||
@ -35,13 +37,15 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
|||||||
PopulateChildren();
|
PopulateChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerBrush layerBrush)
|
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
|
||||||
|
BaseLayerBrush layerBrush)
|
||||||
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
|
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
|
||||||
{
|
{
|
||||||
LayerBrush = layerBrush;
|
LayerBrush = layerBrush;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerEffect layerEffect)
|
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
|
||||||
|
BaseLayerEffect layerEffect)
|
||||||
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
|
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
|
||||||
{
|
{
|
||||||
LayerEffect = layerEffect;
|
LayerEffect = layerEffect;
|
||||||
@ -72,6 +76,21 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
|||||||
set => this.RaiseAndSetIfChanged(ref _hasChildren, value);
|
set => this.RaiseAndSetIfChanged(ref _hasChildren, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(bool expandedOnly)
|
||||||
|
{
|
||||||
|
List<ITimelineKeyframeViewModel> result = new();
|
||||||
|
if (expandedOnly && !IsExpanded)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (ViewModelBase child in Children)
|
||||||
|
if (child is ProfileElementPropertyViewModel profileElementPropertyViewModel)
|
||||||
|
result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels());
|
||||||
|
else if (child is ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel)
|
||||||
|
result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private void PopulateChildren()
|
private void PopulateChildren()
|
||||||
{
|
{
|
||||||
// Get all properties and property groups and create VMs for them
|
// Get all properties and property groups and create VMs for them
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline.TimelineView">
|
||||||
|
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||||
|
<ContentControl Content="{Binding TimelineGroupViewModel}" />
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
|
||||||
|
{
|
||||||
|
public partial class TimelineView : ReactiveUserControl<TimelineViewModel>
|
||||||
|
{
|
||||||
|
public TimelineView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||||
|
|
||||||
|
public class TimelineViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
|
private ObservableAsPropertyHelper<double>? _caretPosition;
|
||||||
|
|
||||||
|
public TimelineViewModel(IProfileEditorService profileEditorService)
|
||||||
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_caretPosition = _profileEditorService.Time
|
||||||
|
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.CaretPosition)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
||||||
|
|
||||||
|
public void ChangeTime(TimeSpan newTime)
|
||||||
|
{
|
||||||
|
_profileEditorService.ChangeTime(newTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
|
||||||
|
{
|
||||||
|
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user