Profile editor - Implemented timeline segments

Profile editor - Added snapping to multiple elements of the timeline
This commit is contained in:
SpoinkyNL committed 2020-07-18 23:52:42 +02:00
1 parent c5219bd224
commit 527fef3dc6
17 files changed
+527 -154

No files matched your search

@@ -1,4 +1,4 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.DisplayConditionsView"
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -6,8 +6,13 @@
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:Converters="clr-namespace:Artemis.UI.Converters" x:Class="Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions.DisplayConditionsView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type local:DisplayConditionsViewModel}}">
<UserControl.Resources>
<Converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -19,9 +24,7 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="10 5 0 -4">
Display conditions
</TextBlock>
<TextBlock Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="10 5 0 -4"><Run Text="Display conditions"/></TextBlock>
<Separator Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource MaterialDesignDarkSeparator}" Margin="8 0" />
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
@@ -31,38 +34,36 @@
</Grid>
<StackPanel Grid.Row="3" Grid.Column="0" Margin="13" Orientation="Horizontal" VerticalAlignment="Bottom" ToolTip="When conditions are met, only go through the entire timeline once.">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}">
Play once
</CheckBox>
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" Content="Play once" IsChecked="{Binding RenderProfileElement.RepeatMainSegment, Converter={StaticResource InverseBooleanConverter}, Mode=OneWay}"/>
</StackPanel>
<StackPanel Grid.Row="3" Grid.Column="1" Margin="10" HorizontalAlignment="Right">
<TextBlock>When conditions no longer met</TextBlock>
<TextBlock><Run Text="When conditions no longer met"/></TextBlock>
<ListBox Style="{StaticResource MaterialDesignToolToggleListBox}" SelectedIndex="{Binding CurrentTimelineIndex}" Height="20" Margin="0 5 0 0">
<ListBoxItem Padding="10 0">
<ListBoxItem.ToolTip>
<ToolTip Placement="Top" VerticalOffset="-5">
<StackPanel>
<TextBlock>When conditions are no longer met, finish the timelines and then stop displaying.</TextBlock>
<TextBlock><Run Text="When conditions are no longer met, finish the timelines and then stop displaying."/></TextBlock>
</StackPanel>
</ToolTip>
</ListBoxItem.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="PlayArrow" Width="20" Height="20" Margin="0 -4" />
<TextBlock Margin="5 0 0 0" FontSize="11">WAIT FOR FINISH</TextBlock>
<TextBlock Margin="5 0 0 0" FontSize="11"><Run Text="WAIT FOR FINISH"/></TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="10 0">
<ListBoxItem.ToolTip>
<ToolTip Placement="Top" VerticalOffset="-5">
<StackPanel>
<TextBlock>When conditions are no longer met, stop displaying immediately.</TextBlock>
<TextBlock><Run Text="When conditions are no longer met, stop displaying immediately."/></TextBlock>
</StackPanel>
</ToolTip>
</ListBoxItem.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="SkipNext" Width="20" Height="20" Margin="0 -4" />
<TextBlock Margin="5 0 0 0" FontSize="11">SKIP</TextBlock>
<TextBlock Margin="5 0 0 0" FontSize="11"><Run Text="SKIP"/></TextBlock>
</StackPanel>
</ListBoxItem>
</ListBox>
@@ -1,4 +1,6 @@
using Artemis.Core.Models.Profile.Conditions;
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.Conditions;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Services.Interfaces;
@@ -9,6 +11,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions
{
private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory;
private DisplayConditionGroupViewModel _rootGroup;
private RenderProfileElement _renderProfileElement;
public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory)
{
@@ -22,8 +25,16 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.DisplayConditions
set => SetAndNotify(ref _rootGroup, value);
}
public RenderProfileElement RenderProfileElement
{
get => _renderProfileElement;
set => SetAndNotify(ref _renderProfileElement, value);
}
private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
{
RenderProfileElement = e.RenderProfileElement;
if (e.RenderProfileElement == null)
{
RootGroup = null;
@@ -199,26 +199,58 @@
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
<!-- Start segment -->
<TextBlock Grid.Column="0" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
<TextBlock Grid.Column="0"
TextAlignment="Center"
VerticalAlignment="Top"
Padding="0 3"
FontSize="11"
Background="{StaticResource MaterialDesignPaper}"
Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
ToolTip="This segment is played when a layer starts displaying because it's conditions are met">
Start
</TextBlock>
<!-- Main segment -->
<TextBlock Grid.Column="1" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
<TextBlock Grid.Column="1"
TextAlignment="Center"
VerticalAlignment="Top"
Padding="0 3"
FontSize="11"
Background="{StaticResource MaterialDesignPaper}"
ToolTip="This segment is played while a condition is met, either once or on a repeating loop">
Main
</TextBlock>
<!-- End segment -->
<TextBlock Grid.Column="2" TextAlignment="Center" VerticalAlignment="Top" Padding="0 3" FontSize="11" Background="{StaticResource MaterialDesignPaper}">
<TextBlock Grid.Column="2"
TextAlignment="Center"
VerticalAlignment="Top"
Padding="0 3"
FontSize="11"
Background="{StaticResource MaterialDesignPaper}"
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
ToolTip="This segment is played once a condition is no longer met">
End
</TextBlock>
<!-- Segment movement display -->
<Rectangle Grid.Column="0" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE" />
HorizontalAlignment="Right" Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<Rectangle Grid.Column="1" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE" />
HorizontalAlignment="Right" />
<Rectangle Grid.Column="2" RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="0 -1 -2 0" Width="5" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE" />
HorizontalAlignment="Right" Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<!-- Segment movement handles -->
<Rectangle Grid.Column="0" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE" Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
MouseDown="{s:Action StartSegmentMouseDown}" MouseUp="{s:Action StartSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
<Rectangle Grid.Column="1" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE"
MouseDown="{s:Action MainSegmentMouseDown}" MouseUp="{s:Action MainSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
<Rectangle Grid.Column="2" RadiusX="2" RadiusY="2" Fill="Transparent" Margin="0 -1 -6 0" Width="16" Height="20" VerticalAlignment="Top"
HorizontalAlignment="Right" Cursor="SizeWE" Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
MouseDown="{s:Action EndSegmentMouseDown}" MouseUp="{s:Action EndSegmentMouseUp}" MouseMove="{s:Action SegmentMouseMove}"/>
</Grid>
</ScrollViewer>
@@ -320,6 +352,16 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding StartSegmentEnabled}">
Enable start segment
</CheckBox>
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" Margin="10 0" IsChecked="{Binding EndSegmentEnabled}">
Enable end segment
</CheckBox>
</StackPanel>
<!-- Zoom control -->
<Slider Grid.Column="1"
Orientation="Horizontal"
@@ -329,6 +371,7 @@
Maximum="350"
TickFrequency="1"
IsSnapToTickEnabled="True"
AutoToolTipPlacement="TopLeft"
Value="{Binding ProfileEditorService.PixelsPerSecond}"
Width="319" />
@@ -94,12 +94,36 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
if (!SetAndNotify(ref _selectedProfileElement, value)) return;
NotifyOfPropertyChange(nameof(SelectedLayer));
NotifyOfPropertyChange(nameof(SelectedFolder));
NotifyOfPropertyChange(nameof(StartSegmentEnabled));
NotifyOfPropertyChange(nameof(EndSegmentEnabled));
}
}
public Layer SelectedLayer => SelectedProfileElement as Layer;
public Folder SelectedFolder => SelectedProfileElement as Folder;
public bool StartSegmentEnabled
{
get => SelectedProfileElement?.StartSegmentLength != TimeSpan.Zero;
set
{
SelectedProfileElement.StartSegmentLength = value ? TimeSpan.FromSeconds(1) : TimeSpan.Zero;
ProfileEditorService.UpdateSelectedProfileElement();
NotifyOfPropertyChange(nameof(StartSegmentEnabled));
}
}
public bool EndSegmentEnabled
{
get => SelectedProfileElement?.EndSegmentLength != TimeSpan.Zero;
set
{
SelectedProfileElement.EndSegmentLength = value ? TimeSpan.FromSeconds(1) : TimeSpan.Zero;
ProfileEditorService.UpdateSelectedProfileElement();
NotifyOfPropertyChange(nameof(EndSegmentEnabled));
}
}
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups
{
get => _layerPropertyGroups;
@@ -123,7 +147,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
get => _timelineViewModel;
set => SetAndNotify(ref _timelineViewModel, value);
}
protected override void OnInitialActivate()
{
PopulateProperties(ProfileEditorService.SelectedProfileElement);
@@ -159,14 +183,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
{
// Placeholder
if (e.RenderProfileElement != null)
{
e.RenderProfileElement.StartSegmentLength = TimeSpan.FromMilliseconds(1000);
e.RenderProfileElement.MainSegmentLength = TimeSpan.FromMilliseconds(5000);
e.RenderProfileElement.EndSegmentLength = TimeSpan.FromMilliseconds(1000);
}
PopulateProperties(e.RenderProfileElement);
}
@@ -176,7 +192,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
NotifyOfPropertyChange(nameof(TimeCaretPosition));
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(TimeCaretPosition));
@@ -538,18 +553,23 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
// If holding down shift, snap to the closest segment or keyframe
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
ProfileEditorService.CurrentTime = newTime;
var snappedTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), true, false, true);
ProfileEditorService.CurrentTime = snappedTime;
return;
}
var visibleKeyframes = GetKeyframes(true);
// If holding down control, round to the closest 50ms
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
var roundedTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
ProfileEditorService.CurrentTime = roundedTime;
return;
}
// Take a tolerance of 5 pixels (half a keyframe width)
var tolerance = 1000f / ProfileEditorService.PixelsPerSecond * 5;
var closeKeyframe = visibleKeyframes.FirstOrDefault(k => Math.Abs(k.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance);
ProfileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime;
ProfileEditorService.CurrentTime = newTime;
}
}
@@ -563,5 +583,98 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
}
#endregion
#region Segments
private bool _draggingStartSegment;
private bool _draggingMainSegment;
private bool _draggingEndSegment;
public void StartSegmentMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
_draggingStartSegment = true;
}
public void StartSegmentMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
_draggingStartSegment = false;
}
public void MainSegmentMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
_draggingMainSegment = true;
}
public void MainSegmentMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
_draggingMainSegment = false;
}
public void EndSegmentMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
_draggingEndSegment = true;
}
public void EndSegmentMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
_draggingEndSegment = false;
}
public void SegmentMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
// Get the parent grid, need that for our position
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
var x = Math.Max(0, e.GetPosition(parent).X);
var newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (ProfileEditorService.PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
// If holding down shift, snap to the closest element on the timeline
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, true);
}
// If holding down control, round to the closest 50ms
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
}
if (_draggingStartSegment)
{
if (newTime < TimeSpan.FromMilliseconds(100))
newTime = TimeSpan.FromMilliseconds(100);
SelectedProfileElement.StartSegmentLength = newTime;
}
else if (_draggingMainSegment)
{
if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100))
newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100);
SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength;
}
else if (_draggingEndSegment)
{
if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100))
newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100);
SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength;
}
}
}
#endregion
}
}
@@ -145,10 +145,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
subds = new object[] {6d, 6d, 30d};
else if (PixelsPerSecond > 150)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 100)
subds = new object[] {4d, 4d, 8d};
else if (PixelsPerSecond > 140)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 90)
subds = new object[] {4d, 4d, 8d};
subds = new object[] {2d, 4d, 20d};
else if (PixelsPerSecond > 60)
subds = new object[] {2d, 4d, 8d};
else if (PixelsPerSecond > 40)
@@ -119,29 +119,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#region Movement
private bool _movementReleased = true;
private TimeSpan _startOffset;
private TimeSpan? _offset;
public void ApplyMovement(TimeSpan cursorTime)
{
if (_movementReleased)
{
_movementReleased = false;
_startOffset = cursorTime - BaseLayerPropertyKeyframe.Position;
}
else
{
BaseLayerPropertyKeyframe.Position = cursorTime - _startOffset;
if (BaseLayerPropertyKeyframe.Position < TimeSpan.Zero)
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
Update(_pixelsPerSecond);
}
UpdatePosition(cursorTime);
Update(_pixelsPerSecond);
}
public void ReleaseMovement()
{
_movementReleased = true;
_offset = null;
}
public void SaveOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
{
if (keyframeViewModel == this)
{
_offset = null;
return;
}
if (_offset != null)
return;
_offset = BaseLayerPropertyKeyframe.Position - keyframeViewModel.BaseLayerPropertyKeyframe.Position;
}
public void ApplyOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
{
if (keyframeViewModel == this || _offset == null)
return;
UpdatePosition(keyframeViewModel.BaseLayerPropertyKeyframe.Position + _offset.Value);
}
private void UpdatePosition(TimeSpan position)
{
if (position < TimeSpan.Zero)
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
else if (position > _profileEditorService.SelectedProfileElement.TimelineLength)
BaseLayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength;
else
BaseLayerPropertyKeyframe.Position = position;
Update(_pixelsPerSecond);
}
#endregion
@@ -13,7 +13,8 @@
Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}">
MouseMove="{s:Action TimelineCanvasMouseMove}"
Margin="0 0 -1 0">
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard>
@@ -32,7 +33,7 @@
</Grid.Triggers>
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
MinWidth="{Binding TotalTimelineWidth}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
@@ -48,7 +49,8 @@
X2="{Binding StartSegmentEndPosition}"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
HorizontalAlignment="Left" />
HorizontalAlignment="Left"
Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
@@ -62,7 +64,8 @@
X1="{Binding EndSegmentEndPosition}"
X2="{Binding EndSegmentEndPosition}"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" />
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
<!-- Multi-selection rectangle -->
<Path x:Name="MultiSelectionPath"
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -27,6 +28,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
LayerPropertyGroups = layerPropertyGroups;
SelectionRectangle = new RectangleGeometry();
_profileEditorService.SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
Update();
@@ -46,10 +48,42 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public double MainSegmentEndPosition => StartSegmentWidth + MainSegmentWidth;
public double EndSegmentWidth => _profileEditorService.PixelsPerSecond * _profileEditorService.SelectedProfileElement?.EndSegmentLength.TotalSeconds ?? 0;
public double EndSegmentEndPosition => StartSegmentWidth + MainSegmentWidth + EndSegmentWidth;
public double TotalTimelineWidth => _profileEditorService.PixelsPerSecond * _profileEditorService.SelectedProfileElement?.TimelineLength.TotalSeconds ?? 0;
public bool StartSegmentEnabled => _profileEditorService.SelectedProfileElement?.StartSegmentLength != TimeSpan.Zero;
public bool EndSegmentEnabled => _profileEditorService.SelectedProfileElement?.EndSegmentLength != TimeSpan.Zero;
public void Dispose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
_profileEditorService.SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
}
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.StartSegmentLength))
{
NotifyOfPropertyChange(nameof(StartSegmentWidth));
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
NotifyOfPropertyChange(nameof(StartSegmentEnabled));
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
}
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.MainSegmentLength))
{
NotifyOfPropertyChange(nameof(MainSegmentWidth));
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
}
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.EndSegmentLength))
{
NotifyOfPropertyChange(nameof(EndSegmentWidth));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
NotifyOfPropertyChange(nameof(EndSegmentEnabled));
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
}
}
public void Update()
@@ -74,6 +108,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
NotifyOfPropertyChange(nameof(EndSegmentWidth));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
}
#region Command handlers
@@ -108,8 +143,12 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public void KeyframeMouseMove(object sender, MouseEventArgs e)
{
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
if (viewModel == null)
return;
if (e.LeftButton == MouseButtonState.Pressed)
MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)));
MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel);
e.Handled = true;
}
@@ -161,6 +200,8 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
var tolerance = 1000f / _profileEditorService.PixelsPerSecond * 5;
if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
time = _profileEditorService.CurrentTime;
else if (Math.Abs(_profileEditorService.SelectedProfileElement.StartSegmentLength.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
time = _profileEditorService.SelectedProfileElement.StartSegmentLength;
}
return time;
@@ -170,14 +211,19 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#region Keyframe movement
public void MoveSelectedKeyframes(TimeSpan cursorTime)
public void MoveSelectedKeyframes(TimeSpan cursorTime, TimelineKeyframeViewModel sourceKeyframeViewModel)
{
// Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging
SelectionRectangle.Rect = new Rect();
var keyframeViewModels = GetAllKeyframeViewModels();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyMovement(cursorTime);
keyframeViewModel.SaveOffsetToKeyframe(sourceKeyframeViewModel);
sourceKeyframeViewModel.ApplyMovement(cursorTime);
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel);
_layerPropertiesViewModel.ProfileEditorService.UpdateProfilePreview();
}