1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Timeline - Added segment time input

Timeline - Ensure segments can't get too short, prevent removing all segments
This commit is contained in:
Robert 2022-07-26 21:26:38 +02:00
parent 92ad3eea92
commit 9135128ffd
10 changed files with 181 additions and 15 deletions

View File

@ -0,0 +1,18 @@
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Dialogs"
xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Dialogs.TimelineSegmentEditView"
x:DataType="dialogs:TimelineSegmentEditViewModel">
<StackPanel>
<controls:NumberBox Name="LengthNumberBox"
Minimum="0.1"
Value="{CompiledBinding SegmentLength}"
HorizontalAlignment="Stretch"
attachedProperties:NumberBoxAssist.SuffixText="sec"/>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public partial class TimelineSegmentEditView : ReactiveUserControl<TimelineSegmentEditViewModel>
{
public TimelineSegmentEditView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,20 @@
using System;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public class TimelineSegmentEditViewModel : ContentDialogViewModelBase
{
private double _segmentLength;
public TimelineSegmentEditViewModel(TimeSpan segmentLength)
{
SegmentLength = segmentLength.TotalSeconds;
}
public double SegmentLength
{
get => _segmentLength;
set => _segmentLength = value;
}
}

View File

@ -15,7 +15,21 @@
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto, Auto,*,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{Binding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
Classes="resize-visual" />

View File

@ -1,6 +1,7 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
@ -16,7 +17,7 @@ public class EndSegmentViewModel : TimelineSegmentViewModel
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
public EndSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
@ -57,6 +58,7 @@ public class EndSegmentViewModel : TimelineSegmentViewModel
{
if (_profileElement != null)
_profileElement.Timeline.EndSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}

View File

@ -15,7 +15,20 @@
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{Binding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
IsVisible="{CompiledBinding !ShowAddStart}"
@ -43,7 +56,7 @@
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding DisableSegment}">
Command="{Binding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>

View File

@ -1,6 +1,7 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
@ -16,7 +17,7 @@ public class MainSegmentViewModel : TimelineSegmentViewModel
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
public MainSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
@ -57,6 +58,7 @@ public class MainSegmentViewModel : TimelineSegmentViewModel
{
if (_profileElement != null)
_profileElement.Timeline.MainSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}

View File

@ -15,6 +15,20 @@
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="*,Auto,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{Binding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"

View File

@ -1,6 +1,7 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
@ -15,7 +16,7 @@ public class StartSegmentViewModel : TimelineSegmentViewModel
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private RenderProfileElement? _profileElement;
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
public StartSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
@ -50,6 +51,7 @@ public class StartSegmentViewModel : TimelineSegmentViewModel
{
if (_profileElement != null)
_profileElement.Timeline.StartSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}

View File

@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
@ -15,18 +20,23 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
{
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private TimeSpan _initialLength;
private readonly Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
private int _pixelsPerSecond;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<bool>? _showAddEnd;
private ObservableAsPropertyHelper<bool>? _showAddMain;
private ObservableAsPropertyHelper<bool>? _showAddStart;
private ReactiveCommand<Unit, Unit> _removeSegment;
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService)
{
_profileEditorService = profileEditorService;
_windowService = windowService;
EditTime = ReactiveCommand.CreateFromTask(ExecuteEditTime);
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
@ -50,6 +60,30 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddEnd)
.DisposeWith(d);
if (Type == ResizeTimelineSegment.SegmentType.Start)
{
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddMain).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddEnd)).Select(tuple => !tuple.First || !tuple.Second)
);
}
if (Type == ResizeTimelineSegment.SegmentType.Main)
{
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddEnd).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddStart)).Select(tuple => !tuple.First || !tuple.Second)
);
}
if (Type == ResizeTimelineSegment.SegmentType.End)
{
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddStart).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddMain)).Select(tuple => !tuple.First || !tuple.Second)
);
}
});
}
@ -66,6 +100,14 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
public abstract string? EndTimestamp { get; }
public abstract ResizeTimelineSegment.SegmentType Type { get; }
public ReactiveCommand<Unit, Unit> EditTime { get; }
public ReactiveCommand<Unit, Unit> RemoveSegment
{
get => _removeSegment;
set => RaiseAndSetIfChanged(ref _removeSegment, value);
}
public void AddStartSegment()
{
if (_profileElement == null)
@ -110,10 +152,11 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
if (_profileElement == null)
return;
TimeSpan difference = GetTimeFromX(x, snap, round) - Length;
TimeSpan time = TimeSpan.FromMilliseconds(Math.Max(GetTimeFromX(x, snap, round).TotalMilliseconds, 100));
TimeSpan difference = time - Length;
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
Length = GetTimeFromX(x, snap, round);
Length = time;
}
public void FinishResize(double x, bool snap, bool round)
@ -121,12 +164,16 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
if (_profileElement == null)
return;
TimeSpan time = TimeSpan.FromMilliseconds(Math.Max(GetTimeFromX(x, snap, round).TotalMilliseconds, 100));
if (_initialLength == time)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x, snap, round), _initialLength));
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, time, _initialLength));
}
public void RemoveSegment()
private void ExecuteRemoveSegment()
{
if (_profileElement == null)
return;
@ -148,17 +195,32 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
}
private async Task ExecuteEditTime()
{
await _windowService.CreateContentDialog()
.WithTitle("Edit segment length")
.WithViewModel(out TimelineSegmentEditViewModel vm, ("segmentLength", Length))
.HavingPrimaryButton(b => b.WithText("Save").WithAction(() =>
{
if (_profileElement != null)
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.FromSeconds(vm.SegmentLength)));
}))
.WithDefaultButton(ContentDialogButton.Primary)
.WithCloseButtonText("Cancel")
.ShowAsync();
}
protected TimeSpan GetTimeFromX(double x, bool snap, bool round)
{
TimeSpan time = TimeSpan.FromSeconds(x / _pixelsPerSecond);
if (time < TimeSpan.Zero)
time = TimeSpan.Zero;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time);
return time;
}
@ -179,7 +241,7 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
_originalKeyframePositions.Clear();
}
private TimeSpan SnapToTimeline(TimeSpan time)
{
TimeSpan tolerance = TimeSpan.FromMilliseconds(1000f / _pixelsPerSecond * 5);