Profile editor - Moved files

This commit is contained in:
SpoinkyNL committed 2020-08-15 17:53:12 +02:00
1 parent 0516ccc658
commit 698d1b8bbd
86 files changed
+199 -215

No files matched your search

@@ -0,0 +1,179 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Controls
{
public class PropertyTimelineHeader : FrameworkElement
{
public static readonly DependencyProperty FillProperty = DependencyProperty.Register(nameof(Fill), typeof(Brush), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty FontFamilyProperty = DependencyProperty.Register(nameof(FontFamily), typeof(FontFamily), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(TextBlock.FontFamilyProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty PixelsPerSecondProperty = DependencyProperty.Register(nameof(PixelsPerSecond), typeof(int), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(nameof(HorizontalOffset), typeof(double), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty VisibleWidthProperty = DependencyProperty.Register(nameof(VisibleWidth), typeof(double), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty OffsetFirstValueProperty = DependencyProperty.Register(nameof(OffsetFirstValue), typeof(bool), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender));
private double _subd1;
private double _subd2;
private double _subd3;
public Brush Fill
{
get => (Brush) GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public FontFamily FontFamily
{
get => (FontFamily) GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
public int PixelsPerSecond
{
get => (int) GetValue(PixelsPerSecondProperty);
set => SetValue(PixelsPerSecondProperty, value);
}
public double HorizontalOffset
{
get => (double) GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
public double VisibleWidth
{
get => (double) GetValue(VisibleWidthProperty);
set => SetValue(VisibleWidthProperty, value);
}
public bool OffsetFirstValue
{
get => (bool) GetValue(OffsetFirstValueProperty);
set => SetValue(OffsetFirstValueProperty, value);
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
UpdateTimeScale();
var linePen = new Pen(Fill, 1);
var width = HorizontalOffset + VisibleWidth;
var frameStart = 0;
var units = PixelsPerSecond / _subd1;
var offsetUnits = frameStart * PixelsPerSecond % units;
// Labels
var count = (width + offsetUnits) / units;
for (var i = 0; i < count; i++)
{
var x = i * units - offsetUnits;
// Add a 100px margin to allow the text to partially render when needed
if (x < HorizontalOffset - 100 || x > HorizontalOffset + width)
continue;
var t = TimeSpan.FromSeconds((i * units - offsetUnits) / PixelsPerSecond + frameStart);
// 0.00 is always formatted as 0.00
if (t == TimeSpan.Zero)
RenderLabel(drawingContext, "0.00", x);
else if (PixelsPerSecond > 200)
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
else if (PixelsPerSecond > 60)
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
else
RenderLabel(drawingContext, $"{Math.Floor(t.TotalMinutes):0}:{t.Seconds:00}", x);
}
// Large ticks
units = PixelsPerSecond / _subd2;
count = (width + offsetUnits) / units;
for (var i = 0; i < count; i++)
{
var x = i * units - offsetUnits;
if (x == 0 && OffsetFirstValue)
drawingContext.DrawLine(linePen, new Point(1, 20), new Point(1, 30));
else if (x > HorizontalOffset && x < HorizontalOffset + width)
drawingContext.DrawLine(linePen, new Point(x, 20), new Point(x, 30));
}
// Small ticks
var mul = _subd3 / _subd2;
units = PixelsPerSecond / _subd3;
count = (width + offsetUnits) / units;
for (var i = 0; i < count; i++)
{
if (Math.Abs(i % mul) < 0.001) continue;
var x = i * units - offsetUnits;
if (x > HorizontalOffset && x < HorizontalOffset + width)
drawingContext.DrawLine(linePen, new Point(x, 25), new Point(x, 30));
}
}
private void RenderLabel(DrawingContext drawingContext, string text, double x)
{
var typeFace = new Typeface(FontFamily, new FontStyle(), new FontWeight(), new FontStretch());
var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, 9, Fill, null, VisualTreeHelper.GetDpi(this).PixelsPerDip);
if (x == 0 && OffsetFirstValue)
drawingContext.DrawText(formattedText, new Point(2, 2));
else
drawingContext.DrawText(formattedText, new Point(x - formattedText.Width / 2, 2));
}
private void UpdateTimeScale()
{
object[] subds;
if (PixelsPerSecond > 350)
subds = new object[] {12d, 12d, 60d};
else if (PixelsPerSecond > 250)
subds = new object[] {6d, 12d, 60d};
else if (PixelsPerSecond > 200)
subds = new object[] {6d, 6d, 30d};
else if (PixelsPerSecond > 150)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 140)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 90)
subds = new object[] {2d, 4d, 20d};
else if (PixelsPerSecond > 60)
subds = new object[] {2d, 4d, 8d};
else if (PixelsPerSecond > 40)
subds = new object[] {1d, 2d, 10d};
else if (PixelsPerSecond > 30)
subds = new object[] {1d, 2d, 10d};
else if (PixelsPerSecond > 10)
subds = new object[] {1d / 2d, 1d / 2d, 1d / 2d};
else if (PixelsPerSecond > 4)
subds = new object[] {1d / 5d, 1d / 5d, 1d / 5d};
else if (PixelsPerSecond > 3)
subds = new object[] {1d / 10d, 1d / 10d, 1d / 5d};
else if (PixelsPerSecond > 1)
subds = new object[] {1d / 20d, 1d / 20d, 1d / 10d};
else if (PixelsPerSecond >= 1)
subds = new object[] {1d / 30d, 1d / 30d, 1d / 15d};
else
{
// 1s per pixel
subds = new object[] {1d / 60d, 1d / 60d, 1d / 15d};
}
_subd1 = (double) subds[0]; // big ticks / labels
_subd2 = (double) subds[1]; // medium ticks
_subd3 = (double) subds[2]; // small ticks
}
}
}
@@ -0,0 +1,45 @@
using System.Windows;
using System.Windows.Media;
using Artemis.Core.Utilities;
using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelineEasingViewModel
{
private readonly TimelineKeyframeViewModel _keyframeViewModel;
private bool _isEasingModeSelected;
public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
{
_keyframeViewModel = keyframeViewModel;
_isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
EasingPoints = new PointCollection();
for (var i = 1; i <= 10; i++)
{
var x = i;
var y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
public Easings.Functions EasingFunction { get; }
public PointCollection EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected
{
get => _isEasingModeSelected;
set
{
_isEasingModeSelected = value;
if (_isEasingModeSelected)
_keyframeViewModel.SelectEasingMode(this);
}
}
}
}
@@ -0,0 +1,191 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Utilities;
using Artemis.UI.Shared.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, LayerPropertyKeyframe<T> layerPropertyKeyframe)
: base(profileEditorService, layerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
LayerPropertyKeyframe = layerPropertyKeyframe;
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
#region Context menu actions
public override void Copy()
{
var newKeyframe = new LayerPropertyKeyframe<T>(
LayerPropertyKeyframe.Value,
LayerPropertyKeyframe.Position,
LayerPropertyKeyframe.EasingFunction,
LayerPropertyKeyframe.LayerProperty
);
// If possible, shift the keyframe to the right by 11 pixels
var desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.TimelineLength)
newKeyframe.Position = desiredPosition;
// Otherwise if possible shift it to the left by 11 pixels
else
{
desiredPosition = newKeyframe.Position - TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
if (desiredPosition > TimeSpan.Zero)
newKeyframe.Position = desiredPosition;
}
LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
public override void Delete()
{
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
}
public abstract class TimelineKeyframeViewModel : PropertyChangedBase, IDisposable
{
private readonly IProfileEditorService _profileEditorService;
private BindableCollection<TimelineEasingViewModel> _easingViewModels;
private bool _isSelected;
private int _pixelsPerSecond;
private string _timestamp;
private double _x;
protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
BaseLayerPropertyKeyframe.PropertyChanged += BaseLayerPropertyKeyframeOnPropertyChanged;
}
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
public BindableCollection<TimelineEasingViewModel> EasingViewModels
{
get => _easingViewModels;
set => SetAndNotify(ref _easingViewModels, value);
}
public bool IsSelected
{
get => _isSelected;
set => SetAndNotify(ref _isSelected, value);
}
public double X
{
get => _x;
set => SetAndNotify(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => SetAndNotify(ref _timestamp, value);
}
public void Update(int pixelsPerSecond)
{
_pixelsPerSecond = pixelsPerSecond;
X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
}
public abstract void Copy();
public abstract void Delete();
private void BaseLayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(BaseLayerPropertyKeyframe.Position))
Update(_pixelsPerSecond);
}
#region Easing
public void CreateEasingViewModels()
{
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
BaseLayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
#region Movement
private TimeSpan? _offset;
public void ReleaseMovement()
{
_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);
}
public 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
public void Dispose()
{
BaseLayerPropertyKeyframe.PropertyChanged -= BaseLayerPropertyKeyframeOnPropertyChanged;
}
}
}
@@ -0,0 +1,74 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelinePropertyGroupView"
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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:layerProperties1="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}"
Visibility="{Binding LayerPropertyGroupViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0"
Height="24"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource MaterialDesignCheckBoxDisabled}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource MaterialDesignDivider}" Height="1" />
<ItemsControl Grid.Row="2"
ItemsSource="{Binding LayerPropertyGroupViewModel.Children}"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style >
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" IsTabStop="False" HorizontalAlignment="Stretch"/>
</DataTemplate>
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}" IsTabStop="False" HorizontalAlignment="Stretch" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Grid>
</UserControl>
@@ -0,0 +1,59 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Shared.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelinePropertyGroupViewModel : PropertyChangedBase
{
private readonly IProfileEditorService _profileEditorService;
private BindableCollection<double> _timelineKeyframeViewModels;
public TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel;
TimelineKeyframeViewModels = new BindableCollection<double>();
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
UpdateKeyframes();
}
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
public BindableCollection<double> TimelineKeyframeViewModels
{
get => _timelineKeyframeViewModels;
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
}
public void UpdateKeyframes()
{
TimelineKeyframeViewModels.Clear();
TimelineKeyframeViewModels.AddRange(LayerPropertyGroupViewModel.GetKeyframes(false)
.Select(k => LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecond * k.Position.TotalSeconds));
}
public void Dispose()
{
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
}
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded))
UpdateKeyframes();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
UpdateKeyframes();
}
}
}
@@ -0,0 +1,117 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView"
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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}"
Visibility="{Binding LayerPropertyBaseViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
MinWidth="{Binding Width}"
HorizontalAlignment="Stretch">
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0"
ToolTip="{Binding Timestamp}"
s:View.ActionTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type timeline:TimelineView}}, Path=DataContext}"
MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}"
ContextMenuOpening="{s:Action ContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
<Ellipse.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" Command="{s:Action Copy}" CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Delete" Command="{s:Action Delete}" CommandParameter="{Binding}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding Path=IsEasingModeSelected, Mode=TwoWay}" />
</Style>
</MenuItem.ItemContainerStyle>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<!-- <MenuItem Header="Easing mode" IsEnabled="{Binding CanSelectEasingMode}"> -->
<!-- <MenuItem Header="Ease in" Command="{s:Action SetEasingMode}" CommandParameter="EaseIn" /> -->
<!-- <MenuItem Header="Ease out" Command="{s:Action SetEasingMode}" CommandParameter="EaseOut" /> -->
<!-- <MenuItem Header="Ease in and out" Command="{s:Action SetEasingMode}" CommandParameter="EaseInOut" /> -->
<!-- </MenuItem> -->
</ContextMenu>
</Ellipse.ContextMenu>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
@@ -0,0 +1,108 @@
using System;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Shared.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelinePropertyViewModel<T> : TimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
{
_profileEditorService = profileEditorService;
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
LayerPropertyViewModel.LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled += LayerPropertyOnKeyframeModified;
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
}
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
public override void UpdateKeyframes()
{
// Only show keyframes if they are enabled
if (LayerPropertyViewModel.LayerProperty.KeyframesEnabled)
{
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
var toRemove = TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe)).ToList();
foreach (var timelineKeyframeViewModel in toRemove)
timelineKeyframeViewModel.Dispose();
TimelineKeyframeViewModels.RemoveRange(toRemove);
TimelineKeyframeViewModels.AddRange(keyframes
.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, k))
);
}
else
DisposeKeyframeViewModels();
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
}
public override void Dispose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyViewModel.LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframeModified;
DisposeKeyframeViewModels();
}
private void DisposeKeyframeViewModels()
{
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Dispose();
TimelineKeyframeViewModels.Clear();
}
private void LayerPropertyOnKeyframeModified(object sender, EventArgs e)
{
UpdateKeyframes();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
Width = TimelineKeyframeViewModels.Any() ? TimelineKeyframeViewModels.Max(t => t.X) + 25 : 0;
}
}
public abstract class TimelinePropertyViewModel : PropertyChangedBase, IDisposable
{
private BindableCollection<TimelineKeyframeViewModel> _timelineKeyframeViewModels;
private double _width;
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
{
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
TimelineKeyframeViewModels = new BindableCollection<TimelineKeyframeViewModel>();
}
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels
{
get => _timelineKeyframeViewModels;
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
}
public double Width
{
get => _width;
set => SetAndNotify(ref _width, value);
}
public abstract void Dispose();
public abstract void UpdateKeyframes();
}
}
@@ -0,0 +1,160 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineSegmentView"
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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type timeline:TimelineSegmentViewModel}}">
<UserControl.Resources>
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Visibility="{Binding SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Header="Repeat main segment"
IsCheckable="True"
IsChecked="{Binding Data.RepeatSegment, Source={StaticResource DataContextProxy}}"
Visibility="{Binding Data.IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Source={StaticResource DataContextProxy}}"/>
<MenuItem Header="Disable segment" Command="{s:Action DisableSegment}"/>
</ContextMenu>
</StackPanel.ContextMenu>
<!-- Segment content -->
<Grid VerticalAlignment="Top" Background="{StaticResource MaterialDesignPaper}" Width="{Binding SegmentWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock TextAlignment="Center"
Padding="0 3"
Margin="5 0 0 0"
FontSize="12"
ToolTip="{Binding ToolTip}"
Text="{Binding Segment}"
ClipToBounds="False">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowSegmentName}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ToggleButton Grid.Column="1"
ToolTip="Toggle main segment repeat"
Width="16"
Height="16"
IsChecked="{Binding RepeatSegment}"
VerticalAlignment="Center"
Visibility="{Binding IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton" BasedOn="{StaticResource MaterialDesignFlatToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowRepeatButton}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" >
<DiscreteObjectKeyFrame KeyTime="00:00:0.25" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<materialDesign:PackIcon Kind="Repeat" Height="12" Width="12" />
</ToggleButton>
<Button Grid.Column="2" ToolTip="Disable segment" Width="20" Height="20" Margin="0 0 5 0" Command="{s:Action DisableSegment}">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignIconButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDisableButton}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<materialDesign:PackIcon Kind="Close" Height="18" Width="18" />
</Button>
</Grid>
<!-- Segment movement display -->
<Rectangle RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="-2 -2 0 0" Width="5" Height="24" VerticalAlignment="Top" HorizontalAlignment="Right" />
<!-- Segment movement handles -->
<Rectangle RadiusX="2"
RadiusY="2"
Fill="Transparent"
Margin="-9 -2 0 0"
Width="12"
Height="25"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Cursor="SizeWE"
MouseDown="{s:Action SegmentMouseDown}"
MouseUp="{s:Action SegmentMouseUp}"
MouseMove="{s:Action SegmentMouseMove}" />
</StackPanel>
</UserControl>
@@ -0,0 +1,302 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Artemis.Core.Models.Profile;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Utilities;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelineSegmentViewModel : PropertyChangedBase, IDisposable
{
private bool _draggingSegment;
private bool _showSegmentName;
private bool _showRepeatButton;
private bool _showDisableButton;
public TimelineSegmentViewModel(IProfileEditorService profileEditorService, SegmentViewModelType segment)
{
ProfileEditorService = profileEditorService;
Segment = segment;
SelectedProfileElement = ProfileEditorService.SelectedProfileElement;
switch (Segment)
{
case SegmentViewModelType.Start:
ToolTip = "This segment is played when a layer starts displaying because it's conditions are met";
break;
case SegmentViewModelType.Main:
ToolTip = "This segment is played while a condition is met, either once or on a repeating loop";
break;
case SegmentViewModelType.End:
ToolTip = "This segment is played once a condition is no longer met";
break;
default:
throw new ArgumentOutOfRangeException(nameof(segment));
}
UpdateDisplay();
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
}
public RenderProfileElement SelectedProfileElement { get; }
public SegmentViewModelType Segment { get; }
public IProfileEditorService ProfileEditorService { get; }
public string ToolTip { get; }
public TimeSpan SegmentLength
{
get
{
return Segment switch
{
SegmentViewModelType.Start => SelectedProfileElement?.StartSegmentLength ?? TimeSpan.Zero,
SegmentViewModelType.Main => SelectedProfileElement?.MainSegmentLength ?? TimeSpan.Zero,
SegmentViewModelType.End => SelectedProfileElement?.EndSegmentLength ?? TimeSpan.Zero,
_ => throw new ArgumentOutOfRangeException()
};
}
}
public double SegmentWidth => ProfileEditorService.PixelsPerSecond * SegmentLength.TotalSeconds;
public bool SegmentEnabled => SegmentLength != TimeSpan.Zero;
public bool IsMainSegment => Segment == SegmentViewModelType.Main;
// Only the main segment supports this, for any other segment the getter always returns false and the setter does nothing
public bool RepeatSegment
{
get
{
if (Segment != SegmentViewModelType.Main)
return false;
return SelectedProfileElement?.DisplayContinuously ?? false;
}
set
{
if (Segment != SegmentViewModelType.Main)
return;
SelectedProfileElement.DisplayContinuously = value;
ProfileEditorService.UpdateSelectedProfileElement();
NotifyOfPropertyChange(nameof(RepeatSegment));
}
}
public double SegmentStartPosition
{
get
{
return Segment switch
{
SegmentViewModelType.Start => 0,
SegmentViewModelType.Main => ProfileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds,
SegmentViewModelType.End => ProfileEditorService.PixelsPerSecond * (SelectedProfileElement.StartSegmentLength.TotalSeconds + SelectedProfileElement.MainSegmentLength.TotalSeconds),
_ => throw new ArgumentOutOfRangeException()
};
}
}
public bool ShowSegmentName
{
get => _showSegmentName;
set => SetAndNotify(ref _showSegmentName, value);
}
public bool ShowRepeatButton
{
get => _showRepeatButton;
set => SetAndNotify(ref _showRepeatButton, value);
}
public bool ShowDisableButton
{
get => _showDisableButton;
set => SetAndNotify(ref _showDisableButton, value);
}
public void Dispose()
{
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
}
public void DisableSegment()
{
var keyframes = SelectedProfileElement.GetAllKeyframes();
var startSegmentEnd = SelectedProfileElement.StartSegmentLength;
var mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
var oldSegmentLength = SegmentLength;
if (Segment == SegmentViewModelType.Start)
{
// Remove keyframes that fall in this segment
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position <= startSegmentEnd))
baseLayerPropertyKeyframe.Remove();
SelectedProfileElement.StartSegmentLength = TimeSpan.Zero;
}
else if (Segment == SegmentViewModelType.Main)
{
// Remove keyframes that fall in this segment
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > startSegmentEnd && k.Position <= mainSegmentEnd))
baseLayerPropertyKeyframe.Remove();
SelectedProfileElement.MainSegmentLength = TimeSpan.Zero;
}
else if (Segment == SegmentViewModelType.End)
{
// Remove keyframes that fall in this segment
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > mainSegmentEnd))
baseLayerPropertyKeyframe.Remove();
SelectedProfileElement.EndSegmentLength = TimeSpan.Zero;
}
NotifyOfPropertyChange(nameof(SegmentEnabled));
ShiftNextSegment(SegmentLength - oldSegmentLength);
ProfileEditorService.UpdateSelectedProfileElement();
}
public void EnableSegment()
{
ShiftNextSegment(TimeSpan.FromSeconds(1));
if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.StartSegmentLength = TimeSpan.FromSeconds(1);
else if (Segment == SegmentViewModelType.Main)
SelectedProfileElement.MainSegmentLength = TimeSpan.FromSeconds(1);
else if (Segment == SegmentViewModelType.End)
SelectedProfileElement.EndSegmentLength = TimeSpan.FromSeconds(1);
NotifyOfPropertyChange(nameof(SegmentEnabled));
ProfileEditorService.UpdateSelectedProfileElement();
UpdateDisplay();
}
public void ShiftNextSegment(TimeSpan amount)
{
var segmentEnd = TimeSpan.Zero;
if (Segment == SegmentViewModelType.Start)
segmentEnd = SelectedProfileElement.StartSegmentLength;
else if (Segment == SegmentViewModelType.Main)
segmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
else if (Segment == SegmentViewModelType.End)
segmentEnd = SelectedProfileElement.TimelineLength;
foreach (var baseLayerPropertyKeyframe in SelectedProfileElement.GetAllKeyframes().Where(k => k.Position > segmentEnd))
baseLayerPropertyKeyframe.Position += amount;
}
public void SegmentMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
_draggingSegment = true;
}
public void SegmentMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
_draggingSegment = false;
ProfileEditorService.UpdateSelectedProfileElement();
}
public void SegmentMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || !_draggingSegment)
return;
// Get the parent scroll viewer, need that for our position
var parent = VisualTreeUtilities.FindParent<ScrollViewer>((DependencyObject) sender, "TimelineHeaderScrollViewer");
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);
var oldSegmentLength = SegmentLength;
if (Segment == SegmentViewModelType.Start)
{
if (newTime < TimeSpan.FromMilliseconds(100))
newTime = TimeSpan.FromMilliseconds(100);
SelectedProfileElement.StartSegmentLength = newTime;
}
else if (Segment == SegmentViewModelType.Main)
{
if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100))
newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100);
SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength;
}
else if (Segment == SegmentViewModelType.End)
{
if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100))
newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100);
SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength;
}
NotifyOfPropertyChange(nameof(SegmentLength));
NotifyOfPropertyChange(nameof(SegmentWidth));
ShiftNextSegment(SegmentLength - oldSegmentLength);
UpdateDisplay();
}
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(RenderProfileElement.StartSegmentLength) ||
e.PropertyName == nameof(RenderProfileElement.MainSegmentLength) ||
e.PropertyName == nameof(RenderProfileElement.EndSegmentLength))
{
NotifyOfPropertyChange(nameof(SegmentStartPosition));
NotifyOfPropertyChange(nameof(SegmentWidth));
}
else if (e.PropertyName == nameof(RenderProfileElement.DisplayContinuously))
NotifyOfPropertyChange(nameof(RepeatSegment));
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(SegmentWidth));
NotifyOfPropertyChange(nameof(SegmentStartPosition));
UpdateDisplay();
}
private void UpdateDisplay()
{
if (!IsMainSegment)
ShowSegmentName = SegmentWidth > 60;
else
ShowSegmentName = SegmentWidth > 80;
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
ShowDisableButton = SegmentWidth > 25;
}
}
public enum SegmentViewModelType
{
Start,
Main,
End
}
}
@@ -0,0 +1,82 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineView"
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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="25"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid x:Name="TimelineContainerGrid"
Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}"
Margin="0 0 -1 0">
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
MinWidth="{Binding TotalTimelineWidth}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" HorizontalContentAlignment="Left" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding StartSegmentEndPosition}"
X2="{Binding StartSegmentEndPosition}"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
HorizontalAlignment="Left"
Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding MainSegmentEndPosition}"
X2="{Binding MainSegmentEndPosition}"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" />
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding EndSegmentEndPosition}"
X2="{Binding EndSegmentEndPosition}"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
<!-- Multi-selection rectangle -->
<Path x:Name="MultiSelectionPath"
Data="{Binding SelectionRectangle}"
Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill>
</Path>
</Grid>
</UserControl>
@@ -0,0 +1,378 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Artemis.Core.Models.Profile;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Utilities;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelineViewModel : PropertyChangedBase, IViewAware, IDisposable
{
private readonly LayerPropertiesViewModel _layerPropertiesViewModel;
private readonly IProfileEditorService _profileEditorService;
private RectangleGeometry _selectionRectangle;
public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups, IProfileEditorService profileEditorService)
{
_layerPropertiesViewModel = layerPropertiesViewModel;
_profileEditorService = profileEditorService;
LayerPropertyGroups = layerPropertyGroups;
SelectionRectangle = new RectangleGeometry();
SelectedProfileElement = layerPropertiesViewModel.SelectedProfileElement;
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
Update();
}
public RenderProfileElement SelectedProfileElement { get; set; }
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
public RectangleGeometry SelectionRectangle
{
get => _selectionRectangle;
set => SetAndNotify(ref _selectionRectangle, value);
}
public double StartSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds;
public double StartSegmentEndPosition => StartSegmentWidth;
public double MainSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.MainSegmentLength.TotalSeconds;
public double MainSegmentEndPosition => StartSegmentWidth + MainSegmentWidth;
public double EndSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.EndSegmentLength.TotalSeconds;
public double EndSegmentEndPosition => StartSegmentWidth + MainSegmentWidth + EndSegmentWidth;
public double TotalTimelineWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.TimelineLength.TotalSeconds;
public bool StartSegmentEnabled => SelectedProfileElement.StartSegmentLength != TimeSpan.Zero;
public bool EndSegmentEnabled => SelectedProfileElement.EndSegmentLength != TimeSpan.Zero;
public void Dispose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
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()
{
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
{
layerPropertyGroupViewModel.TimelinePropertyGroupViewModel.UpdateKeyframes();
foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren())
{
if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel)
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes();
}
}
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(StartSegmentWidth));
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
NotifyOfPropertyChange(nameof(MainSegmentWidth));
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
NotifyOfPropertyChange(nameof(EndSegmentWidth));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
}
#region Command handlers
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
{
// if (e.LeftButton == MouseButtonState.Released)
// return;
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
if (viewModel == null)
return;
((IInputElement) sender).CaptureMouse();
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !viewModel.IsSelected)
SelectKeyframe(viewModel, true, false);
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
SelectKeyframe(viewModel, false, true);
else if (!viewModel.IsSelected)
SelectKeyframe(viewModel, false, false);
e.Handled = true;
}
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
{
_profileEditorService.UpdateSelectedProfileElement();
ReleaseSelectedKeyframes();
((IInputElement) sender).ReleaseMouseCapture();
}
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)), viewModel);
e.Handled = true;
}
#region Context menu actions
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
viewModel?.CreateEasingViewModels();
}
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
viewModel?.EasingViewModels.Clear();
}
public void Copy(TimelineKeyframeViewModel viewModel)
{
viewModel.Copy();
}
public void Delete(TimelineKeyframeViewModel viewModel)
{
viewModel.Delete();
}
#endregion
private TimeSpan GetCursorTime(Point position)
{
// Get the parent grid, need that for our position
var x = Math.Max(0, position.X);
var time = TimeSpan.FromSeconds(x / _profileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (_profileEditorService.PixelsPerSecond < 200)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
else if (_profileEditorService.PixelsPerSecond < 500)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0);
else
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
// If shift is held, snap to the current time
// Take a tolerance of 5 pixels (half a keyframe width)
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
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;
}
#endregion
#region Keyframe movement
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.SaveOffsetToKeyframe(sourceKeyframeViewModel);
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
cursorTime = _profileEditorService.SnapToTimeline(
cursorTime,
TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5),
true,
false,
true,
sourceKeyframeViewModel.BaseLayerPropertyKeyframe
);
}
sourceKeyframeViewModel.UpdatePosition(cursorTime);
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel);
_layerPropertiesViewModel.ProfileEditorService.UpdateProfilePreview();
}
public void ReleaseSelectedKeyframes()
{
var keyframeViewModels = GetAllKeyframeViewModels();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ReleaseMovement();
}
#endregion
#region Keyframe selection
private Point _mouseDragStartPoint;
private bool _mouseDragging;
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
SelectionRectangle.Rect = new Rect();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
_mouseDragging = true;
e.Handled = true;
}
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseUp(object sender, MouseEventArgs e)
{
if (!_mouseDragging)
return;
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
var keyframeViewModels = GetAllKeyframeViewModels();
var selectedKeyframes = HitTestUtilities.GetHitViewModels<TimelineKeyframeViewModel>((Visual) sender, SelectionRectangle);
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel);
_mouseDragging = false;
e.Handled = true;
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineCanvasMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed)
{
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
e.Handled = true;
}
}
public void SelectKeyframe(TimelineKeyframeViewModel clicked, bool selectBetween, bool toggle)
{
var keyframeViewModels = GetAllKeyframeViewModels();
if (selectBetween)
{
var selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected);
// If nothing is selected, select only the clicked
if (selectedIndex == -1)
{
clicked.IsSelected = true;
return;
}
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
var clickedIndex = keyframeViewModels.IndexOf(clicked);
if (clickedIndex < selectedIndex)
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
keyframeViewModel.IsSelected = true;
}
else
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
keyframeViewModel.IsSelected = true;
}
}
else if (toggle)
{
// Toggle only the clicked keyframe, leave others alone
clicked.IsSelected = !clicked.IsSelected;
}
else
{
// Only select the clicked keyframe
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
clicked.IsSelected = true;
}
}
private List<TimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
var viewModels = new List<LayerPropertyBaseViewModel>();
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
viewModels.AddRange(layerPropertyGroupViewModel.GetAllChildren());
var keyframes = viewModels.Where(vm => vm is LayerPropertyViewModel)
.SelectMany(vm => ((LayerPropertyViewModel) vm).TimelinePropertyBaseViewModel.TimelineKeyframeViewModels)
.ToList();
return keyframes;
}
#endregion
#region IViewAware
public void AttachView(UIElement view)
{
if (View != null)
throw new InvalidOperationException(string.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, GetType().Name));
View = view;
}
public UIElement View { get; set; }
#endregion
}
}