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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
@ -59,6 +60,17 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
/// <param name="time">The new time.</param>
|
||||
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>
|
||||
/// Executes the provided command and adds it to the history.
|
||||
/// </summary>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
@ -110,6 +111,42 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
_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)
|
||||
{
|
||||
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
||||
|
||||
@ -6,42 +6,103 @@
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
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,*">
|
||||
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}"></ContentControl>
|
||||
<ScrollViewer Grid.Row="1"
|
||||
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.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 Grid.Column="2" RowDefinitions="48,*">
|
||||
|
||||
<Canvas Grid.Row="0" ZIndex="2">
|
||||
<!-- Timeline segments -->
|
||||
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" />
|
||||
|
||||
<!-- Timeline header body -->
|
||||
<!-- <controls:PropertyTimelineHeader Margin="0 18 0 0" -->
|
||||
<!-- Foreground="{DynamicResource MaterialDesignBody}" -->
|
||||
<!-- Background="{DynamicResource MaterialDesignCardBackground}" -->
|
||||
<!-- 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>
|
||||
|
||||
</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.ReactiveUI;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPropertiesViewModel>
|
||||
{
|
||||
private Polygon _timelineCaret;
|
||||
private Line _timelineLine;
|
||||
|
||||
public ProfileElementPropertiesView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@ -13,5 +24,69 @@ public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPr
|
||||
private void InitializeComponent()
|
||||
{
|
||||
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.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Playback;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
@ -19,17 +20,21 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel> _cachedViewModels;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
|
||||
private ObservableAsPropertyHelper<double>? _pixelsPerSecond;
|
||||
private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
|
||||
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel, TimelineViewModel timelineViewModel)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
_propertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>();
|
||||
_cachedViewModels = new Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel>();
|
||||
PlaybackViewModel = playbackViewModel;
|
||||
TimelineViewModel = timelineViewModel;
|
||||
|
||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
@ -46,13 +51,20 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
||||
.Subscribe(_ => UpdateGroups());
|
||||
// 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());
|
||||
}
|
||||
|
||||
public PlaybackViewModel PlaybackViewModel { get; }
|
||||
public TimelineViewModel TimelineViewModel { get; }
|
||||
public RenderProfileElement? ProfileElement => _profileElement?.Value;
|
||||
public Layer? Layer => _profileElement?.Value as Layer;
|
||||
public double PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||
public IObservable<bool> Playing => _profileEditorService.Playing;
|
||||
|
||||
public ObservableCollection<ProfileElementPropertyGroupViewModel> PropertyGroupViewModels
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@ -6,6 +7,7 @@ using Artemis.Core;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.PropertyInput;
|
||||
@ -35,13 +37,15 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
LayerEffect = layerEffect;
|
||||
@ -72,6 +76,21 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
||||
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()
|
||||
{
|
||||
// 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