diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml new file mode 100644 index 000000000..23dad425d --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml @@ -0,0 +1,18 @@ + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml.cs new file mode 100644 index 000000000..f46564a8a --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditView.axaml.cs @@ -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 +{ + public TimelineSegmentEditView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditViewModel.cs new file mode 100644 index 000000000..a9a4318e7 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Dialogs/TimelineSegmentEditViewModel.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml index f26745983..4b81a53c7 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml @@ -15,7 +15,21 @@ Background="{DynamicResource ControlFillColorDefaultBrush}" Width="{CompiledBinding Width}" ColumnDefinitions="Auto, Auto,*,Auto"> - + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs index ab48333a6..7e10e7999 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentViewModel.cs @@ -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? _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)); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml index 222038f88..9325e0d67 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml @@ -15,7 +15,20 @@ Background="{DynamicResource ControlFillColorDefaultBrush}" Width="{CompiledBinding Width}" ColumnDefinitions="Auto,Auto,*,Auto,Auto"> - + + + + + + + + + + + + + + + Command="{Binding RemoveSegment}"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs index 3fa9c266f..15ebb371c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs @@ -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? _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)); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml index 1a9ffedc4..b58b57539 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml @@ -15,6 +15,20 @@ Background="{DynamicResource ControlFillColorDefaultBrush}" Width="{CompiledBinding Width}" ColumnDefinitions="*,Auto,Auto"> + + + + + + + + + + + + + + ? _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)); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs index 92e9154f4..da58970a6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs @@ -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 _originalKeyframePositions = new(); private int _pixelsPerSecond; private RenderProfileElement? _profileElement; private ObservableAsPropertyHelper? _showAddEnd; private ObservableAsPropertyHelper? _showAddMain; - private ObservableAsPropertyHelper? _showAddStart; + private ReactiveCommand _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 EditTime { get; } + + public ReactiveCommand 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 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);