mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Timeline - Implemented segments
This commit is contained in:
parent
913117ad0a
commit
642823add5
@ -102,6 +102,20 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
void UpdateDataBinding();
|
||||
|
||||
/// <summary>
|
||||
/// Removes a keyframe from the layer property without knowing it's type.
|
||||
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
|
||||
/// </summary>
|
||||
/// <param name="keyframe"></param>
|
||||
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a keyframe to the layer property without knowing it's type.
|
||||
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
|
||||
/// </summary>
|
||||
/// <param name="keyframe"></param>
|
||||
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the layer property is disposed
|
||||
/// </summary>
|
||||
|
||||
@ -96,6 +96,24 @@ namespace Artemis.Core
|
||||
OnUpdated();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe)
|
||||
{
|
||||
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
|
||||
throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}.");
|
||||
|
||||
RemoveKeyframe(typedKeyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe)
|
||||
{
|
||||
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
|
||||
throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}.");
|
||||
|
||||
AddKeyframe(typedKeyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
@ -177,22 +195,25 @@ namespace Artemis.Core
|
||||
/// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new
|
||||
/// or existing keyframe.
|
||||
/// </param>
|
||||
public void SetCurrentValue(T value, TimeSpan? time)
|
||||
/// <returns>The new keyframe if one was created.</returns>
|
||||
public LayerPropertyKeyframe<T>? SetCurrentValue(T value, TimeSpan? time)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
|
||||
LayerPropertyKeyframe<T>? newKeyframe = null;
|
||||
if (time == null || !KeyframesEnabled || !KeyframesSupported)
|
||||
{
|
||||
BaseValue = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If on a keyframe, update the keyframe
|
||||
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
|
||||
// Create a new keyframe if none found
|
||||
if (currentKeyframe == null)
|
||||
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this));
|
||||
{
|
||||
newKeyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
|
||||
AddKeyframe(newKeyframe);
|
||||
}
|
||||
else
|
||||
currentKeyframe.Value = value;
|
||||
}
|
||||
@ -200,6 +221,7 @@ namespace Artemis.Core
|
||||
// Force an update so that the base value is applied to the current value and
|
||||
// keyframes/data bindings are applied using the new base value
|
||||
ReapplyUpdate();
|
||||
return newKeyframe;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -247,6 +269,7 @@ namespace Artemis.Core
|
||||
{
|
||||
if (_keyframesEnabled == value) return;
|
||||
_keyframesEnabled = value;
|
||||
ReapplyUpdate();
|
||||
OnKeyframesToggled();
|
||||
OnPropertyChanged(nameof(KeyframesEnabled));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to delete a keyframe.
|
||||
/// </summary>
|
||||
public class DeleteKeyframe<T> : IProfileEditorCommand
|
||||
public class DeleteKeyframe : IProfileEditorCommand
|
||||
{
|
||||
private readonly LayerPropertyKeyframe<T> _keyframe;
|
||||
private readonly ILayerPropertyKeyframe _keyframe;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
|
||||
/// </summary>
|
||||
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||
public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
|
||||
{
|
||||
_keyframe = keyframe;
|
||||
}
|
||||
@ -25,13 +25,13 @@ public class DeleteKeyframe<T> : IProfileEditorCommand
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_keyframe.LayerProperty.RemoveKeyframe(_keyframe);
|
||||
_keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_keyframe.LayerProperty.AddKeyframe(_keyframe);
|
||||
_keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to change the length of a timeline segment.
|
||||
/// </summary>
|
||||
public class ResizeTimelineSegment : IProfileEditorCommand
|
||||
{
|
||||
private readonly TimeSpan _length;
|
||||
private readonly TimeSpan _originalLength;
|
||||
private readonly RenderProfileElement _profileElement;
|
||||
private readonly SegmentType _segmentType;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
|
||||
/// </summary>
|
||||
/// <param name="segmentType">The type of segment to resize.</param>
|
||||
/// <param name="profileElement">The render profile element whose segment to resize.</param>
|
||||
/// <param name="length">The new length of the segment</param>
|
||||
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length)
|
||||
{
|
||||
_segmentType = segmentType;
|
||||
_profileElement = profileElement;
|
||||
_length = length;
|
||||
_originalLength = _segmentType switch
|
||||
{
|
||||
SegmentType.Start => _profileElement.Timeline.StartSegmentLength,
|
||||
SegmentType.Main => _profileElement.Timeline.MainSegmentLength,
|
||||
SegmentType.End => _profileElement.Timeline.EndSegmentLength,
|
||||
_ => _originalLength
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
|
||||
/// </summary>
|
||||
/// <param name="segmentType">The type of segment to resize.</param>
|
||||
/// <param name="profileElement">The render profile element whose segment to resize.</param>
|
||||
/// <param name="length">The new length of the segment</param>
|
||||
/// <param name="originalLength">The original length of the segment</param>
|
||||
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length, TimeSpan originalLength)
|
||||
{
|
||||
_segmentType = segmentType;
|
||||
_profileElement = profileElement;
|
||||
_length = length;
|
||||
_originalLength = originalLength;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => $"Resize {_segmentType.ToString().ToLower()} segment";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
switch (_segmentType)
|
||||
{
|
||||
case SegmentType.Start:
|
||||
_profileElement.Timeline.StartSegmentLength = _length;
|
||||
break;
|
||||
case SegmentType.Main:
|
||||
_profileElement.Timeline.MainSegmentLength = _length;
|
||||
break;
|
||||
case SegmentType.End:
|
||||
_profileElement.Timeline.EndSegmentLength = _length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
switch (_segmentType)
|
||||
{
|
||||
case SegmentType.Start:
|
||||
_profileElement.Timeline.StartSegmentLength = _originalLength;
|
||||
break;
|
||||
case SegmentType.Main:
|
||||
_profileElement.Timeline.MainSegmentLength = _originalLength;
|
||||
break;
|
||||
case SegmentType.End:
|
||||
_profileElement.Timeline.EndSegmentLength = _originalLength;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a type of segment on a timeline.
|
||||
/// </summary>
|
||||
public enum SegmentType
|
||||
{
|
||||
/// <summary>
|
||||
/// The start segment.
|
||||
/// </summary>
|
||||
Start,
|
||||
|
||||
/// <summary>
|
||||
/// The main segment.
|
||||
/// </summary>
|
||||
Main,
|
||||
|
||||
/// <summary>
|
||||
/// The end segment.
|
||||
/// </summary>
|
||||
End
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
|
||||
private readonly T _newValue;
|
||||
private readonly T _originalValue;
|
||||
private readonly TimeSpan? _time;
|
||||
private LayerPropertyKeyframe<T>? _newKeyframe;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
||||
@ -43,13 +44,16 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_newValue, _time);
|
||||
_newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||
if (_newKeyframe != null)
|
||||
_layerProperty.RemoveKeyframe(_newKeyframe);
|
||||
else
|
||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@ -8,6 +8,7 @@ using Artemis.UI.Screens.ProfileEditor;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
||||
using Artemis.UI.Screens.Settings;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
@ -76,11 +77,6 @@ namespace Artemis.UI.Ninject.Factories
|
||||
|
||||
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
|
||||
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
||||
|
||||
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
|
||||
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
}
|
||||
|
||||
public interface IPropertyVmFactory
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||
</UserControl.Styles>
|
||||
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
|
||||
<Grid RowDefinitions="48,*">
|
||||
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
|
||||
@ -31,76 +34,103 @@
|
||||
Background="Transparent"
|
||||
Margin="0 0 -5 0" />
|
||||
|
||||
<Grid Grid.Column="2" RowDefinitions="48,*">
|
||||
<!-- Timeline header body -->
|
||||
<controls:TimelineHeader Grid.Row="0"
|
||||
Name="TimelineHeader"
|
||||
Margin="0 18 0 0"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
||||
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
||||
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
||||
OffsetFirstValue="True"
|
||||
PointerReleased="TimelineHeader_OnPointerReleased"
|
||||
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
||||
Cursor="Hand" />
|
||||
<!-- Horizontal scrolling -->
|
||||
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<Grid RowDefinitions="48,*">
|
||||
<!-- Timeline header body -->
|
||||
<controls:TimelineHeader Grid.Row="0"
|
||||
Name="TimelineHeader"
|
||||
Margin="0 18 0 0"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
||||
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
||||
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
||||
OffsetFirstValue="True"
|
||||
PointerReleased="TimelineHeader_OnPointerReleased"
|
||||
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
||||
Cursor="Hand" />
|
||||
|
||||
<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}" />
|
||||
<Canvas Grid.Row="0" ZIndex="2">
|
||||
<!-- Segment dividers -->
|
||||
<Line Name="TimelineLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||
Cursor="SizeWestEast"
|
||||
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}">
|
||||
</Line>
|
||||
|
||||
<!-- 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}" />
|
||||
</Line.RenderTransform>
|
||||
</Line>
|
||||
</Canvas>
|
||||
<Line Name="StartSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
<Line Name="MainSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
<Line Name="EndSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
|
||||
<!-- Horizontal scrolling -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Name="TimelineScrollViewer"
|
||||
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
||||
<ContentControl Content="{Binding TimelineViewModel}" />
|
||||
</ScrollViewer>
|
||||
<!-- Timeline segments -->
|
||||
<ContentControl Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.StartX}"
|
||||
Classes="segment-content-control"
|
||||
Content="{Binding TimelineViewModel.EndSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.StartX}"
|
||||
Classes="segment-content-control"
|
||||
Content="{Binding TimelineViewModel.MainSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.StartX}"
|
||||
Classes="segment-content-control"
|
||||
Content="{Binding TimelineViewModel.StartSegmentViewModel}" />
|
||||
|
||||
<!-- TODO: Databindings here -->
|
||||
<!-- 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>
|
||||
</Canvas>
|
||||
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Name="TimelineScrollViewer"
|
||||
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
||||
VerticalScrollBarVisibility="Hidden">
|
||||
<ContentControl Content="{Binding TimelineViewModel}"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- TODO: Databindings here -->
|
||||
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ using Artemis.Core.LayerBrushes;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public interface ITimelineKeyframeViewModel
|
||||
{
|
||||
@ -3,7 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineEasingView">
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
StrokeThickness="1"
|
||||
@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
|
||||
{
|
||||
public partial class TimelineEasingView : UserControl
|
||||
{
|
||||
@ -4,7 +4,7 @@ using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public class TimelineEasingViewModel : ViewModelBase
|
||||
{
|
||||
@ -5,7 +5,7 @@
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineKeyframeView"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
|
||||
ClipToBounds="False"
|
||||
Height="{DynamicResource RailsHeight}">
|
||||
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
|
||||
@ -4,7 +4,7 @@ using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
||||
{
|
||||
@ -7,11 +7,10 @@ using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Input;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
||||
{
|
||||
@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe));
|
||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -0,0 +1,56 @@
|
||||
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||
</UserControl.Styles>
|
||||
<Border Classes="segment-container">
|
||||
<Grid Name="SegmentGrid"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||
Width="{Binding Width}"
|
||||
ColumnDefinitions="Auto, Auto,*,Auto">
|
||||
|
||||
<Rectangle Name="KeyframeDragVisualLeft"
|
||||
Grid.Column="0"
|
||||
Classes="resize-visual" />
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Name="AddMainSegment"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
ToolTip.Tip="Add main segment"
|
||||
Command="{Binding AddMainSegment}"
|
||||
IsVisible="{Binding ShowAddMain}">
|
||||
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
|
||||
<TextBlock Name="SegmentTitle"
|
||||
FontSize="13"
|
||||
ToolTip.Tip="This segment is played once a condition is no longer met">
|
||||
End
|
||||
</TextBlock>
|
||||
<Button Name="SegmentClose"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
ToolTip.Tip="Remove this segment"
|
||||
Command="{Binding RemoveSegment}">
|
||||
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle Name="KeyframeDragVisualRight"
|
||||
Grid.Column="3"
|
||||
Classes="resize-visual" />
|
||||
<Rectangle Name="KeyframeDragAnchor"
|
||||
Grid.Column="3"
|
||||
Classes="resize-anchor"
|
||||
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@ -0,0 +1,50 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||
{
|
||||
public partial class EndSegmentView : ReactiveUserControl<EndSegmentViewModel>
|
||||
{
|
||||
private readonly Rectangle _keyframeDragAnchor;
|
||||
private double _dragOffset;
|
||||
|
||||
public EndSegmentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
e.Pointer.Capture(_keyframeDragAnchor);
|
||||
|
||||
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||
ViewModel.StartResize();
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
e.Pointer.Capture(null);
|
||||
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
|
||||
public class EndSegmentViewModel : TimelineSegmentViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RenderProfileElement? _profileElement;
|
||||
private int _pixelsPerSecond;
|
||||
private TimeSpan _time;
|
||||
private ObservableAsPropertyHelper<double>? _start;
|
||||
private ObservableAsPropertyHelper<double>? _end;
|
||||
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||
private readonly ObservableAsPropertyHelper<double> _width;
|
||||
private TimeSpan _initialLength;
|
||||
|
||||
|
||||
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||
|
||||
_start = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentStartPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.StartX)
|
||||
.DisposeWith(d);
|
||||
_end = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.EndX)
|
||||
.DisposeWith(d);
|
||||
_endTimestamp = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||
.ToProperty(this, vm => vm.EndTimestamp)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||
}
|
||||
|
||||
public override TimeSpan Start => _profileElement?.Timeline.EndSegmentStartPosition ?? TimeSpan.Zero;
|
||||
public override double StartX => _start?.Value ?? 0;
|
||||
public override TimeSpan End => _profileElement?.Timeline.EndSegmentEndPosition ?? TimeSpan.Zero;
|
||||
public override double EndX => _end?.Value ?? 0;
|
||||
public override TimeSpan Length
|
||||
{
|
||||
get => _profileElement?.Timeline.EndSegmentLength ?? TimeSpan.Zero;
|
||||
set
|
||||
{
|
||||
if (_profileElement != null)
|
||||
_profileElement.Timeline.EndSegmentLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override double Width => _width.Value;
|
||||
|
||||
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||
</UserControl.Styles>
|
||||
<Border Classes="segment-container">
|
||||
<Grid Name="SegmentGrid"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||
Width="{Binding Width}"
|
||||
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="KeyframeDragVisualLeft"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding !ShowAddStart}"
|
||||
Classes="resize-visual" />
|
||||
|
||||
<Button Name="AddStartSegment"
|
||||
Grid.Column="1"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip.Tip="Add a start segment"
|
||||
Command="{Binding AddStartSegment}"
|
||||
IsVisible="{Binding ShowAddStart}">
|
||||
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Name="SegmentTitle"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="13"
|
||||
ToolTip.Tip="This segment is played while a condition is met, either once or on a repeating loop">
|
||||
Main
|
||||
</TextBlock>
|
||||
|
||||
<Button Name="SegmentClose"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
ToolTip.Tip="Remove this segment"
|
||||
Command="{Binding DisableSegment}">
|
||||
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||
</Button>
|
||||
<ToggleButton Name="SegmentRepeat"
|
||||
Classes="icon-button icon-button-small"
|
||||
ToolTip.Tip="Repeat this segment"
|
||||
IsChecked="{Binding RepeatSegment}"
|
||||
Padding="0">
|
||||
<avalonia:MaterialIcon Kind="Repeat" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
|
||||
<Button Name="AddEndSegment"
|
||||
Grid.Column="3"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip.Tip="Add an end segment"
|
||||
Command="{Binding AddEndSegment}"
|
||||
IsVisible="{Binding ShowAddEnd}">
|
||||
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||
</Button>
|
||||
|
||||
<Rectangle Name="KeyframeDragVisual"
|
||||
Grid.Column="4"
|
||||
Classes="resize-visual" />
|
||||
<Rectangle Name="KeyframeDragAnchor"
|
||||
Grid.Column="4"
|
||||
Classes="resize-anchor"
|
||||
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@ -0,0 +1,62 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||
{
|
||||
public partial class MainSegmentView : ReactiveUserControl<MainSegmentViewModel>
|
||||
{
|
||||
private readonly Rectangle _keyframeDragAnchor;
|
||||
private double _dragOffset;
|
||||
|
||||
public MainSegmentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
e.Pointer.Capture(_keyframeDragAnchor);
|
||||
|
||||
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||
ViewModel.StartResize();
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
e.Pointer.Capture(null);
|
||||
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
|
||||
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
|
||||
public class MainSegmentViewModel : TimelineSegmentViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RenderProfileElement? _profileElement;
|
||||
private int _pixelsPerSecond;
|
||||
private TimeSpan _time;
|
||||
private ObservableAsPropertyHelper<double>? _start;
|
||||
private ObservableAsPropertyHelper<double>? _end;
|
||||
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||
private readonly ObservableAsPropertyHelper<double> _width;
|
||||
private TimeSpan _initialLength;
|
||||
|
||||
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||
|
||||
_start = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.StartX)
|
||||
.DisposeWith(d);
|
||||
_end = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.EndX)
|
||||
.DisposeWith(d);
|
||||
_endTimestamp = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||
.ToProperty(this, vm => vm.EndTimestamp)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||
}
|
||||
|
||||
public override TimeSpan Start => _profileElement?.Timeline.MainSegmentStartPosition ?? TimeSpan.Zero;
|
||||
public override double StartX => _start?.Value ?? 0;
|
||||
public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero;
|
||||
public override double EndX => _end?.Value ?? 0;
|
||||
public override TimeSpan Length
|
||||
{
|
||||
get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero;
|
||||
set
|
||||
{
|
||||
if (_profileElement != null)
|
||||
_profileElement.Timeline.MainSegmentLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override double Width => _width.Value;
|
||||
|
||||
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20">
|
||||
<!-- Add Controls for Previewer Here -->
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Style Selector="ContentControl.segment-content-control">
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.segment-container">
|
||||
<Setter Property="Height" Value="18"></Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Rectangle.resize-visual">
|
||||
<Setter Property="Fill" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||
<Setter Property="Width" Value="4"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Rectangle.resize-anchor">
|
||||
<Setter Property="Fill" Value="Transparent"/>
|
||||
<Setter Property="Width" Value="10"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter Property="Cursor" Value="SizeWestEast"/>
|
||||
</Style>
|
||||
</Styles>
|
||||
@ -0,0 +1,54 @@
|
||||
<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:system="clr-namespace:System;assembly=System.Runtime"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||
</UserControl.Styles>
|
||||
<Border Classes="segment-container">
|
||||
<Grid Name="SegmentGrid"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||
Width="{Binding Width}"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Name="SegmentTitle"
|
||||
FontSize="13"
|
||||
ToolTip.Tip="This segment is played when a layer starts displaying because it's conditions are met">
|
||||
Start
|
||||
</TextBlock>
|
||||
<Button Name="SegmentClose"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
ToolTip.Tip="Remove this segment"
|
||||
Command="{Binding RemoveSegment}">
|
||||
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Name="AddMainSegment"
|
||||
Classes="AppBarButton icon-button icon-button-small"
|
||||
ToolTip.Tip="Add main segment"
|
||||
Command="{Binding AddMainSegment}"
|
||||
IsVisible="{Binding ShowAddMain}">
|
||||
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||
</Button>
|
||||
|
||||
<Rectangle Name="KeyframeDragVisual"
|
||||
Grid.Column="2"
|
||||
Classes="resize-visual" />
|
||||
<Rectangle Name="KeyframeDragAnchor"
|
||||
Grid.Column="2"
|
||||
Classes="resize-anchor"
|
||||
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@ -0,0 +1,50 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||
{
|
||||
public partial class StartSegmentView : ReactiveUserControl<StartSegmentViewModel>
|
||||
{
|
||||
private readonly Rectangle _keyframeDragAnchor;
|
||||
private double _dragOffset;
|
||||
|
||||
public StartSegmentView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
e.Pointer.Capture(_keyframeDragAnchor);
|
||||
|
||||
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||
ViewModel.StartResize();
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
|
||||
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||
return;
|
||||
e.Pointer.Capture(null);
|
||||
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Castle.DynamicProxy.Generators;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
|
||||
public class StartSegmentViewModel : TimelineSegmentViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RenderProfileElement? _profileElement;
|
||||
private int _pixelsPerSecond;
|
||||
private TimeSpan _time;
|
||||
private ObservableAsPropertyHelper<double>? _end;
|
||||
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||
private readonly ObservableAsPropertyHelper<double> _width;
|
||||
private TimeSpan _initialLength;
|
||||
|
||||
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||
|
||||
_end = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.EndX)
|
||||
.DisposeWith(d);
|
||||
_endTimestamp = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||
.ToProperty(this, vm => vm.EndTimestamp)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||
}
|
||||
|
||||
public override TimeSpan Start => TimeSpan.Zero;
|
||||
public override double StartX => 0;
|
||||
public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero;
|
||||
public override double EndX => _end?.Value ?? 0;
|
||||
public override TimeSpan Length
|
||||
{
|
||||
get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero;
|
||||
set
|
||||
{
|
||||
if (_profileElement != null)
|
||||
_profileElement.Timeline.StartSegmentLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override double Width => _width.Value;
|
||||
|
||||
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
|
||||
public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RenderProfileElement? _profileElement;
|
||||
private int _pixelsPerSecond;
|
||||
private Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
|
||||
|
||||
private ObservableAsPropertyHelper<bool>? _showAddStart;
|
||||
private ObservableAsPropertyHelper<bool>? _showAddMain;
|
||||
private ObservableAsPropertyHelper<bool>? _showAddEnd;
|
||||
private TimeSpan _initialLength;
|
||||
|
||||
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||
|
||||
_showAddStart = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(t => t == TimeSpan.Zero)
|
||||
.ToProperty(this, vm => vm.ShowAddStart)
|
||||
.DisposeWith(d);
|
||||
_showAddMain = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(t => t == TimeSpan.Zero)
|
||||
.ToProperty(this, vm => vm.ShowAddMain)
|
||||
.DisposeWith(d);
|
||||
_showAddEnd = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.Select(t => t == TimeSpan.Zero)
|
||||
.ToProperty(this, vm => vm.ShowAddEnd)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowAddStart => _showAddStart?.Value ?? false;
|
||||
public bool ShowAddMain => _showAddMain?.Value ?? false;
|
||||
public bool ShowAddEnd => _showAddEnd?.Value ?? false;
|
||||
|
||||
public abstract TimeSpan Start { get; }
|
||||
public abstract double StartX { get; }
|
||||
public abstract TimeSpan End { get; }
|
||||
public abstract double EndX { get; }
|
||||
public abstract TimeSpan Length { get; set; }
|
||||
public abstract double Width { get; }
|
||||
public abstract string? EndTimestamp { get; }
|
||||
public abstract ResizeTimelineSegment.SegmentType Type { get; }
|
||||
|
||||
public void AddStartSegment()
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment");
|
||||
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength);
|
||||
ApplyPendingKeyframeMovement();
|
||||
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength));
|
||||
}
|
||||
|
||||
public void AddMainSegment()
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment");
|
||||
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength);
|
||||
ApplyPendingKeyframeMovement();
|
||||
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength));
|
||||
}
|
||||
|
||||
public void AddEndSegment()
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment");
|
||||
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength));
|
||||
}
|
||||
|
||||
public void StartResize()
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
_initialLength = Length;
|
||||
}
|
||||
|
||||
public void UpdateResize(double x)
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
TimeSpan difference = GetTimeFromX(x) - Length;
|
||||
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
|
||||
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
|
||||
Length = GetTimeFromX(x);
|
||||
}
|
||||
|
||||
public void FinishResize(double x)
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
|
||||
ApplyPendingKeyframeMovement();
|
||||
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength));
|
||||
}
|
||||
|
||||
public void RemoveSegment()
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment");
|
||||
IEnumerable<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes);
|
||||
|
||||
// Delete keyframes in the segment
|
||||
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
|
||||
if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End)
|
||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe));
|
||||
|
||||
// Move keyframes after the segment forwards
|
||||
ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1));
|
||||
ApplyPendingKeyframeMovement();
|
||||
|
||||
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
protected TimeSpan GetTimeFromX(double x)
|
||||
{
|
||||
TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond);
|
||||
if (length < TimeSpan.Zero)
|
||||
length = TimeSpan.Zero;
|
||||
return length;
|
||||
}
|
||||
|
||||
protected void ShiftKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, TimeSpan amount)
|
||||
{
|
||||
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
|
||||
{
|
||||
if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe))
|
||||
_originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position;
|
||||
layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount);
|
||||
}
|
||||
}
|
||||
|
||||
protected void ApplyPendingKeyframeMovement()
|
||||
{
|
||||
foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions)
|
||||
_profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition));
|
||||
|
||||
_originalKeyframePositions.Clear();
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,8 @@
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
||||
<Style Selector="ItemsControl > ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
</ItemsControl>
|
||||
|
||||
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.Controls.Mixins;
|
||||
@ -24,6 +25,9 @@ public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelineP
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
|
||||
.Subscribe(_ => UpdateKeyframes())
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
|
||||
.Subscribe(_ => UpdateKeyframes())
|
||||
.DisposeWith(d);
|
||||
|
||||
@ -10,8 +10,11 @@
|
||||
<x:Double x:Key="RailsHeight">28</x:Double>
|
||||
<x:Double x:Key="RailsBorderHeight">29</x:Double>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased">
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
|
||||
<Grid.KeyBindings>
|
||||
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
|
||||
</Grid.KeyBindings>
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl Content="{Binding TimelineGroupViewModel}" />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Shared.Controls;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Artemis.UI.Shared.Extensions;
|
||||
|
||||
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
private ObservableAsPropertyHelper<double>? _caretPosition;
|
||||
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
|
||||
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
|
||||
private ObservableAsPropertyHelper<double> _minWidth;
|
||||
|
||||
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
|
||||
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
|
||||
StartSegmentViewModel startSegmentViewModel,
|
||||
MainSegmentViewModel mainSegmentViewModel,
|
||||
EndSegmentViewModel endSegmentViewModel,
|
||||
IProfileEditorService profileEditorService)
|
||||
{
|
||||
PropertyGroupViewModels = propertyGroupViewModels;
|
||||
StartSegmentViewModel = startSegmentViewModel;
|
||||
MainSegmentViewModel = mainSegmentViewModel;
|
||||
EndSegmentViewModel = endSegmentViewModel;
|
||||
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.CaretPosition)
|
||||
.DisposeWith(d);
|
||||
|
||||
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
|
||||
_minWidth = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
|
||||
.Switch()
|
||||
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
|
||||
.ToProperty(this, vm => vm.MinWidth)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
|
||||
public StartSegmentViewModel StartSegmentViewModel { get; }
|
||||
public MainSegmentViewModel MainSegmentViewModel { get; }
|
||||
public EndSegmentViewModel EndSegmentViewModel { get; }
|
||||
|
||||
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
||||
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||
public double MinWidth => _minWidth?.Value ?? 0;
|
||||
|
||||
public void ChangeTime(TimeSpan newTime)
|
||||
{
|
||||
@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
|
||||
#region Keyframe actions
|
||||
|
||||
public void DuplicateKeyframes(ITimelineKeyframeViewModel source)
|
||||
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||
{
|
||||
if (!source.IsSelected)
|
||||
source.Duplicate();
|
||||
if (source is { IsSelected: false })
|
||||
source.Delete();
|
||||
else
|
||||
{
|
||||
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||
@ -131,9 +152,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyKeyframes(ITimelineKeyframeViewModel source)
|
||||
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||
{
|
||||
if (!source.IsSelected)
|
||||
if (source is { IsSelected: false })
|
||||
source.Copy();
|
||||
else
|
||||
{
|
||||
@ -144,9 +165,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public void PasteKeyframes(ITimelineKeyframeViewModel source)
|
||||
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||
{
|
||||
if (!source.IsSelected)
|
||||
if (source is { IsSelected: false })
|
||||
source.Paste();
|
||||
else
|
||||
{
|
||||
@ -157,9 +178,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteKeyframes(ITimelineKeyframeViewModel source)
|
||||
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||
{
|
||||
if (!source.IsSelected)
|
||||
if (source is {IsSelected: false})
|
||||
source.Delete();
|
||||
else
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user