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:
parent
92ad3eea92
commit
9135128ffd
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user